From 7150b9314ddbc906597de5df0faf08e636a18d5f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 22 Jul 2025 01:35:02 +0400 Subject: [PATCH 01/79] feat(external-migrations): vault migrations --- .../routes/v3/external-migration-router.ts | 28 ++ backend/src/server/routes/v3/index.ts | 2 +- .../envkey.ts} | 361 +----------------- .../external-migration-fns/import.ts | 352 +++++++++++++++++ .../external-migration-fns/index.ts | 3 + .../external-migration-fns/vault.ts | 312 +++++++++++++++ .../external-migration-queue.ts | 3 +- .../external-migration-service.ts | 64 +++- .../external-migration-types.ts | 27 +- frontend/src/hooks/api/migration/index.ts | 1 + .../src/hooks/api/migration/mutations.tsx | 22 +- .../SelectImportFromPlatformModal.tsx | 46 ++- .../components/VaultPlatformModal copy.tsx | 226 +++++++++++ 13 files changed, 1075 insertions(+), 372 deletions(-) rename backend/src/services/external-migration/{external-migration-fns.ts => external-migration-fns/envkey.ts} (61%) create mode 100644 backend/src/services/external-migration/external-migration-fns/import.ts create mode 100644 backend/src/services/external-migration/external-migration-fns/index.ts create mode 100644 backend/src/services/external-migration/external-migration-fns/vault.ts create mode 100644 frontend/src/hooks/api/migration/index.ts create mode 100644 frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx diff --git a/backend/src/server/routes/v3/external-migration-router.ts b/backend/src/server/routes/v3/external-migration-router.ts index 865287157..d7aca92e2 100644 --- a/backend/src/server/routes/v3/external-migration-router.ts +++ b/backend/src/server/routes/v3/external-migration-router.ts @@ -1,9 +1,11 @@ import fastifyMultipart from "@fastify/multipart"; +import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { VaultMappingType } from "@app/services/external-migration/external-migration-types"; const MB25_IN_BYTES = 26214400; @@ -52,4 +54,30 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider }); } }); + + server.route({ + method: "POST", + url: "/vault", + config: { + rateLimit: readLimit + }, + schema: { + body: z.object({ + vaultAccessToken: z.string(), + vaultNamespace: z.string(), + vaultUrl: z.string(), + mappingType: z.nativeEnum(VaultMappingType) + }) + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + await server.services.migration.importVaultData({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body + }); + } + }); }; diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index ed8401560..1c31741a1 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -11,5 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => { await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); - await server.register(registerExternalMigrationRouter, { prefix: "/migrate" }); + await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" }); }; diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns/envkey.ts similarity index 61% rename from backend/src/services/external-migration/external-migration-fns.ts rename to backend/src/services/external-migration/external-migration-fns/envkey.ts index 8af22d858..ffdf80e9c 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns/envkey.ts @@ -1,32 +1,26 @@ -import slugify from "@sindresorhus/slugify"; import sjcl from "sjcl"; import tweetnacl from "tweetnacl"; import tweetnaclUtil from "tweetnacl-util"; -import { SecretType, TSecretFolders } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { chunkArray } from "@app/lib/fn"; +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; -import { TKmsServiceFactory } from "../kms/kms-service"; -import { KmsDataKey } from "../kms/kms-types"; -import { TProjectDALFactory } from "../project/project-dal"; -import { TProjectServiceFactory } from "../project/project-service"; -import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; -import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; -import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; -import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; -import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; -import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; -import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; -import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns"; -import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service"; -import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; -import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; -import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types"; +import { TFolderCommitServiceFactory } from "../../folder-commit/folder-commit-service"; +import { TKmsServiceFactory } from "../../kms/kms-service"; +import { TProjectDALFactory } from "../../project/project-dal"; +import { TProjectServiceFactory } from "../../project/project-service"; +import { TProjectEnvDALFactory } from "../../project-env/project-env-dal"; +import { TProjectEnvServiceFactory } from "../../project-env/project-env-service"; +import { TResourceMetadataDALFactory } from "../../resource-metadata/resource-metadata-dal"; +import { TSecretFolderDALFactory } from "../../secret-folder/secret-folder-dal"; +import { TSecretFolderVersionDALFactory } from "../../secret-folder/secret-folder-version-dal"; +import { TSecretTagDALFactory } from "../../secret-tag/secret-tag-dal"; +import { TSecretV2BridgeDALFactory } from "../../secret-v2-bridge/secret-v2-bridge-dal"; +import type { TSecretV2BridgeServiceFactory } from "../../secret-v2-bridge/secret-v2-bridge-service"; +import { TSecretVersionV2DALFactory } from "../../secret-v2-bridge/secret-version-dal"; +import { TSecretVersionV2TagDALFactory } from "../../secret-v2-bridge/secret-version-tag-dal"; +import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "../external-migration-types"; export type TImportDataIntoInfisicalDTO = { projectDAL: Pick; @@ -499,326 +493,3 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise { - // Import data to infisical - if (!data || !data.projects) { - throw new BadRequestError({ message: "No projects found in data" }); - } - - const originalToNewProjectId = new Map(); - const originalToNewEnvironmentId = new Map< - string, - { envId: string; envSlug: string; rootFolderId: string; projectId: string } - >(); - const originalToNewFolderId = new Map< - string, - { - folderId: string; - projectId: string; - } - >(); - const projectsNotImported: string[] = []; - - await projectDAL.transaction(async (tx) => { - for await (const project of data.projects) { - const newProject = await projectService - .createProject({ - actor, - actorId, - actorOrgId, - actorAuthMethod, - workspaceName: project.name, - createDefaultEnvs: false, - tx - }) - .catch((e) => { - logger.error(e, `Failed to import to project [name:${project.name}]`); - throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` }); - }); - originalToNewProjectId.set(project.id, newProject.id); - } - - // Import environments - if (data.environments) { - for await (const environment of data.environments) { - const projectId = originalToNewProjectId.get(environment.projectId); - const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`); - - if (!projectId) { - projectsNotImported.push(environment.projectId); - // eslint-disable-next-line no-continue - continue; - } - - const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx); - - if (existingEnv) { - throw new BadRequestError({ - message: `Environment with slug '${slug}' already exist`, - name: "CreateEnvironment" - }); - } - - const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx); - const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx); - const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx); - - originalToNewEnvironmentId.set(environment.id, { - envSlug: doc.slug, - envId: doc.id, - rootFolderId: folder.id, - projectId - }); - } - } - - if (data.folders) { - for await (const folder of data.folders) { - const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string); - - if (!parentEnv) { - // eslint-disable-next-line no-continue - continue; - } - - const newFolder = await folderDAL.create( - { - name: folder.name, - envId: parentEnv.envId, - parentId: parentEnv.rootFolderId - }, - tx - ); - - const newFolderVersion = await folderVersionDAL.create( - { - name: newFolder.name, - envId: newFolder.envId, - version: newFolder.version, - folderId: newFolder.id - }, - tx - ); - - await folderCommitService.createCommit( - { - actor: { - type: actor, - metadata: { - id: actorId - } - }, - message: "Changed by external migration", - folderId: parentEnv.rootFolderId, - changes: [ - { - type: CommitType.ADD, - folderVersionId: newFolderVersion.id - } - ] - }, - tx - ); - - originalToNewFolderId.set(folder.id, { - folderId: newFolder.id, - projectId: parentEnv.projectId - }); - } - } - - // Useful for debugging: - // console.log("data.secrets", data.secrets); - // console.log("data.folders", data.folders); - // console.log("data.environment", data.environments); - - if (data.secrets && data.secrets.length > 0) { - const mappedToEnvironmentId = new Map< - string, - { - secretKey: string; - secretValue: string; - folderId?: string; - isFromBlock?: boolean; - }[] - >(); - - for (const secret of data.secrets) { - const targetId = secret.folderId || secret.environmentId; - - // Skip if we can't find either an environment or folder mapping for this secret - if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) { - logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret"); - - // eslint-disable-next-line no-continue - continue; - } - - if (!mappedToEnvironmentId.has(targetId)) { - mappedToEnvironmentId.set(targetId, []); - } - - const alreadyHasSecret = mappedToEnvironmentId - .get(targetId)! - .find((el) => el.secretKey === secret.name && el.folderId === secret.folderId); - - if (alreadyHasSecret && alreadyHasSecret.isFromBlock) { - // remove the existing secret if any - mappedToEnvironmentId - .get(targetId)! - .splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1); - } - mappedToEnvironmentId.get(targetId)!.push({ - secretKey: secret.name, - secretValue: secret.value || "", - folderId: secret.folderId, - isFromBlock: secret.appBlockOrderIndex !== undefined - }); - } - - // for each of the mappedEnvironmentId - for await (const [targetId, secrets] of mappedToEnvironmentId) { - logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId); - - let selectedFolder: TSecretFolders | undefined; - let selectedProjectId: string | undefined; - - // Case 1: Secret belongs to a folder / branch / branch of a block - const foundFolder = originalToNewFolderId.get(targetId); - if (foundFolder) { - logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder"); - selectedFolder = await folderDAL.findById(foundFolder.folderId, tx); - selectedProjectId = foundFolder.projectId; - } else { - logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment"); - const environment = data.environments.find((env) => env.id === targetId); - if (!environment) { - logger.info( - { - targetId - }, - "[importDataIntoInfisicalFn]: Could not find environment for secret" - ); - // eslint-disable-next-line no-continue - continue; - } - - const projectId = originalToNewProjectId.get(environment.projectId)!; - - if (!projectId) { - throw new BadRequestError({ message: `Failed to import secret, project not found` }); - } - - const env = originalToNewEnvironmentId.get(targetId); - if (!env) { - logger.info( - { - targetId - }, - "[importDataIntoInfisicalFn]: Could not find environment for secret" - ); - - // eslint-disable-next-line no-continue - continue; - } - - const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx); - - if (!folder) { - throw new NotFoundError({ - message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`, - name: "Create secret" - }); - } - - selectedFolder = folder; - selectedProjectId = projectId; - } - - if (!selectedFolder) { - throw new NotFoundError({ - message: `Folder not found for the given environment slug & secret path`, - name: "CreateSecret" - }); - } - - if (!selectedProjectId) { - throw new NotFoundError({ - message: `Project not found for the given environment slug & secret path`, - name: "CreateSecret" - }); - } - - const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey( - { - type: KmsDataKey.SecretManager, - projectId: selectedProjectId - }, - tx - ); - - const secretBatches = chunkArray(secrets, 2500); - for await (const secretBatch of secretBatches) { - const secretsByKeys = await secretDAL.findBySecretKeys( - selectedFolder.id, - secretBatch.map((el) => ({ - key: el.secretKey, - type: SecretType.Shared - })), - tx - ); - if (secretsByKeys.length) { - throw new BadRequestError({ - message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}` - }); - } - await fnSecretBulkInsert({ - inputSecrets: secretBatch.map((el) => { - const references = getAllSecretReferences(el.secretValue).nestedReferences; - - return { - version: 1, - encryptedValue: el.secretValue - ? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob - : undefined, - key: el.secretKey, - references, - type: SecretType.Shared - }; - }), - folderId: selectedFolder.id, - orgId: actorOrgId, - resourceMetadataDAL, - secretDAL, - secretVersionDAL, - secretTagDAL, - secretVersionTagDAL, - folderCommitService, - actor: { - type: actor, - actorId - }, - tx - }); - } - } - } - }); - - return { projectsNotImported }; -}; diff --git a/backend/src/services/external-migration/external-migration-fns/import.ts b/backend/src/services/external-migration/external-migration-fns/import.ts new file mode 100644 index 000000000..5728bf1c0 --- /dev/null +++ b/backend/src/services/external-migration/external-migration-fns/import.ts @@ -0,0 +1,352 @@ +import slugify from "@sindresorhus/slugify"; + +import { SecretType, TSecretFolders } from "@app/db/schemas"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { chunkArray } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { CommitType } from "@app/services/folder-commit/folder-commit-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; +import { fnSecretBulkInsert, getAllSecretReferences } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns"; + +import { TImportDataIntoInfisicalDTO } from "./envkey"; + +export const importDataIntoInfisicalFn = async ({ + projectService, + projectEnvDAL, + projectDAL, + secretDAL, + kmsService, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + folderDAL, + resourceMetadataDAL, + folderVersionDAL, + folderCommitService, + input: { data, actor, actorId, actorOrgId, actorAuthMethod } +}: TImportDataIntoInfisicalDTO) => { + // Import data to infisical + if (!data || !data.projects) { + throw new BadRequestError({ message: "No projects found in data" }); + } + + const originalToNewProjectId = new Map(); + const originalToNewEnvironmentId = new Map< + string, + { envId: string; envSlug: string; rootFolderId?: string; projectId: string } + >(); + const originalToNewFolderId = new Map< + string, + { + envId: string; + envSlug: string; + folderId: string; + projectId: string; + } + >(); + const projectsNotImported: string[] = []; + + await projectDAL.transaction(async (tx) => { + for await (const project of data.projects) { + const newProject = await projectService + .createProject({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + workspaceName: project.name, + createDefaultEnvs: false, + tx + }) + .catch((e) => { + logger.error(e, `Failed to import to project [name:${project.name}]`); + throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` }); + }); + originalToNewProjectId.set(project.id, newProject.id); + } + + // Import environments + if (data.environments) { + for await (const environment of data.environments) { + const projectId = originalToNewProjectId.get(environment.projectId); + const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`); + + if (!projectId) { + projectsNotImported.push(environment.projectId); + // eslint-disable-next-line no-continue + continue; + } + + const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx); + + if (existingEnv) { + throw new BadRequestError({ + message: `Environment with slug '${slug}' already exist`, + name: "CreateEnvironment" + }); + } + + const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx); + const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx); + const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx); + + originalToNewEnvironmentId.set(environment.id, { + envSlug: doc.slug, + envId: doc.id, + rootFolderId: folder.id, + projectId + }); + } + } + + if (data.folders) { + for await (const folder of data.folders) { + const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string); + const parentFolder = originalToNewFolderId.get(folder.parentFolderId as string); + + let newFolder: TSecretFolders; + + if (parentEnv?.rootFolderId) { + newFolder = await folderDAL.create( + { + name: folder.name, + envId: parentEnv.envId, + parentId: parentEnv.rootFolderId + }, + tx + ); + } else if (parentFolder) { + newFolder = await folderDAL.create( + { + name: folder.name, + envId: parentFolder.envId, + parentId: parentFolder.folderId + }, + tx + ); + } else { + logger.info({ folder }, "No parent environment found for folder"); + // eslint-disable-next-line no-continue + continue; + } + + const newFolderVersion = await folderVersionDAL.create( + { + name: newFolder.name, + envId: newFolder.envId, + version: newFolder.version, + folderId: newFolder.id + }, + tx + ); + + await folderCommitService.createCommit( + { + actor: { + type: actor, + metadata: { + id: actorId + } + }, + message: "Changed by external migration", + folderId: parentEnv?.rootFolderId || parentFolder?.folderId || "", + changes: [ + { + type: CommitType.ADD, + folderVersionId: newFolderVersion.id + } + ] + }, + tx + ); + + originalToNewFolderId.set(folder.id, { + folderId: newFolder.id, + envId: parentEnv?.envId || parentFolder?.envId || "", + envSlug: parentEnv?.envSlug || parentFolder?.envSlug || "", + projectId: parentEnv?.projectId || parentFolder?.projectId || "" + }); + } + } + + // Useful for debugging: + // console.log("data.secrets", data.secrets); + // console.log("data.folders", data.folders); + // console.log("data.environment", data.environments); + + if (data.secrets && data.secrets.length > 0) { + const mappedToEnvironmentId = new Map< + string, + { + secretKey: string; + secretValue: string; + folderId?: string; + isFromBlock?: boolean; + }[] + >(); + + for (const secret of data.secrets) { + const targetId = secret.folderId || secret.environmentId; + + // Skip if we can't find either an environment or folder mapping for this secret + if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) { + logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret"); + + // eslint-disable-next-line no-continue + continue; + } + + if (!mappedToEnvironmentId.has(targetId)) { + mappedToEnvironmentId.set(targetId, []); + } + + const alreadyHasSecret = mappedToEnvironmentId + .get(targetId)! + .find((el) => el.secretKey === secret.name && el.folderId === secret.folderId); + + if (alreadyHasSecret && alreadyHasSecret.isFromBlock) { + // remove the existing secret if any + mappedToEnvironmentId + .get(targetId)! + .splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1); + } + mappedToEnvironmentId.get(targetId)!.push({ + secretKey: secret.name, + secretValue: secret.value || "", + folderId: secret.folderId, + isFromBlock: secret.appBlockOrderIndex !== undefined + }); + } + + // for each of the mappedEnvironmentId + for await (const [targetId, secrets] of mappedToEnvironmentId) { + logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId); + + let selectedFolder: TSecretFolders | undefined; + let selectedProjectId: string | undefined; + + // Case 1: Secret belongs to a folder / branch / branch of a block + const foundFolder = originalToNewFolderId.get(targetId); + if (foundFolder) { + logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder"); + selectedFolder = await folderDAL.findById(foundFolder.folderId, tx); + selectedProjectId = foundFolder.projectId; + } else { + logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment"); + const environment = data.environments.find((env) => env.id === targetId); + if (!environment) { + logger.info( + { + targetId + }, + "[importDataIntoInfisicalFn]: Could not find environment for secret" + ); + // eslint-disable-next-line no-continue + continue; + } + + const projectId = originalToNewProjectId.get(environment.projectId)!; + + if (!projectId) { + throw new BadRequestError({ message: `Failed to import secret, project not found` }); + } + + const env = originalToNewEnvironmentId.get(targetId); + if (!env) { + logger.info( + { + targetId + }, + "[importDataIntoInfisicalFn]: Could not find environment for secret" + ); + + // eslint-disable-next-line no-continue + continue; + } + + const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx); + + if (!folder) { + throw new NotFoundError({ + message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`, + name: "Create secret" + }); + } + + selectedFolder = folder; + selectedProjectId = projectId; + } + + if (!selectedFolder) { + throw new NotFoundError({ + message: `Folder not found for the given environment slug & secret path`, + name: "CreateSecret" + }); + } + + if (!selectedProjectId) { + throw new NotFoundError({ + message: `Project not found for the given environment slug & secret path`, + name: "CreateSecret" + }); + } + + const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey( + { + type: KmsDataKey.SecretManager, + projectId: selectedProjectId + }, + tx + ); + + const secretBatches = chunkArray(secrets, 2500); + for await (const secretBatch of secretBatches) { + const secretsByKeys = await secretDAL.findBySecretKeys( + selectedFolder.id, + secretBatch.map((el) => ({ + key: el.secretKey, + type: SecretType.Shared + })), + tx + ); + if (secretsByKeys.length) { + throw new BadRequestError({ + message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}` + }); + } + await fnSecretBulkInsert({ + inputSecrets: secretBatch.map((el) => { + const references = getAllSecretReferences(el.secretValue).nestedReferences; + + return { + version: 1, + encryptedValue: el.secretValue + ? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob + : undefined, + key: el.secretKey, + references, + type: SecretType.Shared + }; + }), + folderId: selectedFolder.id, + orgId: actorOrgId, + resourceMetadataDAL, + secretDAL, + secretVersionDAL, + secretTagDAL, + secretVersionTagDAL, + folderCommitService, + actor: { + type: actor, + actorId + }, + tx + }); + } + } + } + }); + + return { projectsNotImported }; +}; diff --git a/backend/src/services/external-migration/external-migration-fns/index.ts b/backend/src/services/external-migration/external-migration-fns/index.ts new file mode 100644 index 000000000..4af82bf22 --- /dev/null +++ b/backend/src/services/external-migration/external-migration-fns/index.ts @@ -0,0 +1,3 @@ +export * from "./envkey"; +export * from "./import"; +export * from "./vault"; diff --git a/backend/src/services/external-migration/external-migration-fns/vault.ts b/backend/src/services/external-migration/external-migration-fns/vault.ts new file mode 100644 index 000000000..a61b2e703 --- /dev/null +++ b/backend/src/services/external-migration/external-migration-fns/vault.ts @@ -0,0 +1,312 @@ +import axios, { AxiosInstance } from "axios"; +import { v4 as uuidv4 } from "uuid"; + +import { InfisicalImportData, VaultMappingType } from "../external-migration-types"; + +type VaultData = { + namespace: string; + mount: string; + path: string; + secretData: Record; +}; + +const vaultFactory = () => { + const getMounts = async (request: AxiosInstance) => { + const response = await request.get< + Record< + string, + { + accessor: string; + options: { + version?: string; + } | null; + type: string; + } + > + >("/v1/sys/mounts"); + return response.data; + }; + + const getPaths = async ( + request: AxiosInstance, + { mountPath, secretPath = "" }: { mountPath: string; secretPath?: string } + ) => { + try { + // For KV v2: /v1/{mount}/metadata/{path}?list=true + const path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`; + const response = await request.get<{ + data: { + keys: string[]; + }; + }>(`/v1/${path}?list=true`); + + return response.data.data.keys; + } catch (err) { + if (axios.isAxiosError(err) && err.response?.status === 404) { + return null; + } + throw err; + } + }; + + const getSecrets = async ( + request: AxiosInstance, + { mountPath, secretPath }: { mountPath: string; secretPath: string } + ) => { + // For KV v2: /v1/{mount}/data/{path} + const response = await request.get<{ + data: { + data: Record; // KV v2 has nested data structure + metadata: { + created_time: string; + deletion_time: string; + destroyed: boolean; + version: number; + }; + }; + }>(`/v1/${mountPath}/data/${secretPath}`); + + return response.data.data.data; + }; + + // helper function to check if a mount is KV v2 (will be useful if we add support for Vault KV v1) + // const isKvV2Mount = (mountInfo: { type: string; options?: { version?: string } | null }) => { + // return mountInfo.type === "kv" && mountInfo.options?.version === "2"; + // }; + + const recursivelyGetAllPaths = async ( + request: AxiosInstance, + mountPath: string, + currentPath: string = "" + ): Promise => { + const paths = await getPaths(request, { mountPath, secretPath: currentPath }); + + if (paths === null || paths.length === 0) { + return []; + } + + const allSecrets: string[] = []; + + for await (const path of paths) { + const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path; + const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath; + + if (path.endsWith("/")) { + // it's a folder so we recurse into it + const subSecrets = await recursivelyGetAllPaths(request, mountPath, fullItemPath); + allSecrets.push(...subSecrets); + } else { + // it's a secret so we add it to our results + allSecrets.push(`${mountPath}/${fullItemPath}`); + } + } + + return allSecrets; + }; + + async function collectVaultData({ + baseUrl, + namespace, + accessToken + }: { + baseUrl: string; + namespace: string; + accessToken: string; + }): Promise { + const request = axios.create({ + baseURL: baseUrl, + headers: { + "X-Vault-Namespace": namespace, + "X-Vault-Token": accessToken + } + }); + + const allData: VaultData[] = []; + + // Get all mounts in this namespace + const mounts = await getMounts(request); + + for (const mount of Object.keys(mounts)) { + if (!mount.endsWith("/")) { + delete mounts[mount]; + } + } + + for await (const [mountPath, mountInfo] of Object.entries(mounts)) { + // skip non-KV mounts + if (!mountInfo.type.startsWith("kv")) { + // eslint-disable-next-line no-continue + continue; + } + + // get all paths in this mount + const paths = await recursivelyGetAllPaths(request, `${mountPath.replace(/\/$/, "")}`); + + const cleanMountPath = mountPath.replace(/\/$/, ""); + + for await (const secretPath of paths) { + // get the actual secret data + const secretData = await getSecrets(request, { + mountPath: cleanMountPath, + secretPath: secretPath.replace(`${cleanMountPath}/`, "") + }); + + allData.push({ + namespace, + mount: mountPath.replace(/\/$/, ""), + path: secretPath.replace(`${cleanMountPath}/`, ""), + secretData + }); + } + } + + return allData; + } + + return { + collectVaultData, + getMounts, + getPaths, + getSecrets, + recursivelyGetAllPaths + }; +}; + +export const transformToInfisicalFormatNamespaceToProjects = ( + vaultData: VaultData[], + mappingType: VaultMappingType +): InfisicalImportData => { + const projects: Array<{ name: string; id: string }> = []; + const environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }> = []; + const folders: Array<{ id: string; name: string; environmentId: string; parentFolderId?: string }> = []; + const secrets: Array<{ id: string; name: string; environmentId: string; value: string; folderId?: string }> = []; + + // track created entities to avoid duplicates + const projectMap = new Map(); // namespace -> projectId + const environmentMap = new Map(); // namespace:mount -> environmentId + const folderMap = new Map(); // namespace:mount:folderPath -> folderId + + let environmentId: string = ""; + for (const data of vaultData) { + const { namespace, mount, path, secretData } = data; + + if (mappingType === "namespace") { + // create project (namespace) + if (!projectMap.has(namespace)) { + const projectId = uuidv4(); + projectMap.set(namespace, projectId); + projects.push({ + name: namespace, + id: projectId + }); + } + const projectId = projectMap.get(namespace)!; + + // create environment (mount) + const envKey = `${namespace}:${mount}`; + if (!environmentMap.has(envKey)) { + environmentId = uuidv4(); + environmentMap.set(envKey, environmentId); + environments.push({ + name: mount, + id: environmentId, + projectId + }); + } + environmentId = environmentMap.get(envKey)!; + } else if (mappingType === "key-vault") { + if (!projectMap.has(mount)) { + const projectId = uuidv4(); + projectMap.set(mount, projectId); + projects.push({ + name: mount, + id: projectId + }); + } + const projectId = projectMap.get(mount)!; + + // create single "Production" environment per project, because we have no good way of determining environments from vault + if (!environmentMap.has(mount)) { + environmentId = uuidv4(); + environmentMap.set(mount, environmentId); + environments.push({ + name: "Production", + id: environmentId, + projectId + }); + } + environmentId = environmentMap.get(mount)!; + } + + // create folder structure + let currentFolderId: string | undefined; + let currentPath = ""; + + if (path.includes("/")) { + const pathParts = path.split("/").filter(Boolean); + + const folderParts = pathParts; + + // create nested folder structure for the entire path + for (const folderName of folderParts) { + currentPath = currentPath ? `${currentPath}/${folderName}` : folderName; + const folderKey = `${namespace}:${mount}:${currentPath}`; + + if (!folderMap.has(folderKey)) { + const folderId = uuidv4(); + folderMap.set(folderKey, folderId); + folders.push({ + id: folderId, + name: folderName, + environmentId, + parentFolderId: currentFolderId || environmentId + }); + currentFolderId = folderId; + } else { + currentFolderId = folderMap.get(folderKey)!; + } + } + } + + for (const [key, value] of Object.entries(secretData)) { + secrets.push({ + id: uuidv4(), + name: key, + environmentId, + value: String(value), + folderId: currentFolderId + }); + } + } + + return { + projects, + environments, + folders, + secrets + }; +}; + +export const importVaultDataFn = async ({ + vaultAccessToken, + vaultNamespace, + vaultUrl, + mappingType +}: { + vaultAccessToken: string; + vaultNamespace: string; + vaultUrl: string; + mappingType: VaultMappingType; +}) => { + const vaultApi = vaultFactory(); + + const vaultData = await vaultApi.collectVaultData({ + accessToken: vaultAccessToken, + baseUrl: vaultUrl, + namespace: vaultNamespace + }); + + const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType); + + return infisicalData; +}; diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index c4e6b43c5..8bde91aa5 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -19,7 +19,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { importDataIntoInfisicalFn } from "./external-migration-fns"; -import { ExternalPlatforms, TImportInfisicalDataCreate } from "./external-migration-types"; +import { ExternalPlatforms, ImportType, TImportInfisicalDataCreate } from "./external-migration-types"; export type TExternalMigrationQueueFactoryDep = { smtpService: TSmtpService; @@ -67,6 +67,7 @@ export const externalMigrationQueueFactory = ({ const startImport = async (dto: { actorEmail: string; data: { + importType: ImportType; iv: string; tag: string; ciphertext: string; diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index c310fd273..766b6cef4 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -4,9 +4,9 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { TUserDALFactory } from "../user/user-dal"; -import { decryptEnvKeyDataFn, parseEnvKeyDataFn } from "./external-migration-fns"; +import { decryptEnvKeyDataFn, importVaultDataFn, parseEnvKeyDataFn } from "./external-migration-fns"; import { TExternalMigrationQueueFactory } from "./external-migration-queue"; -import { TImportEnvKeyDataCreate } from "./external-migration-types"; +import { ImportType, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types"; type TExternalMigrationServiceFactoryDep = { permissionService: TPermissionServiceFactory; @@ -28,7 +28,7 @@ export const externalMigrationServiceFactory = ({ actorId, actorOrgId, actorAuthMethod - }: TImportEnvKeyDataCreate) => { + }: TImportEnvKeyDataDTO) => { if (crypto.isFipsModeEnabled()) { throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." }); } @@ -60,11 +60,65 @@ export const externalMigrationServiceFactory = ({ await externalMigrationQueue.startImport({ actorEmail: user.email!, - data: encrypted + data: { + importType: ImportType.EnvKey, + ...encrypted + } + }); + }; + + const importVaultData = async ({ + vaultAccessToken, + vaultNamespace, + mappingType, + vaultUrl, + actor, + actorId, + actorOrgId, + actorAuthMethod + }: TImportVaultDataDTO) => { + const { membership } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + if (membership.role !== OrgMembershipRole.Admin) { + throw new ForbiddenRequestError({ message: "Only admins can import data" }); + } + + const user = await userDAL.findById(actorId); + + const vaultData = await importVaultDataFn({ + vaultAccessToken, + vaultNamespace, + vaultUrl, + mappingType + }); + + const stringifiedJson = JSON.stringify({ + data: vaultData, + actor, + actorId, + actorOrgId, + actorAuthMethod + }); + + const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson); + + await externalMigrationQueue.startImport({ + actorEmail: user.email!, + data: { + importType: ImportType.Vault, + ...encrypted + } }); }; return { - importEnvKeyData + importEnvKeyData, + importVaultData }; }; diff --git a/backend/src/services/external-migration/external-migration-types.ts b/backend/src/services/external-migration/external-migration-types.ts index 32c70a688..f3431b5c9 100644 --- a/backend/src/services/external-migration/external-migration-types.ts +++ b/backend/src/services/external-migration/external-migration-types.ts @@ -1,5 +1,17 @@ +import { TOrgPermission } from "@app/lib/types"; + import { ActorAuthMethod, ActorType } from "../auth/auth-type"; +export enum ImportType { + EnvKey = "envkey", + Vault = "vault" +} + +export enum VaultMappingType { + Namespace = "namespace", + KeyVault = "key-vault" +} + export type InfisicalImportData = { projects: Array<{ name: string; id: string }>; environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }>; @@ -14,14 +26,17 @@ export type InfisicalImportData = { }>; }; -export type TImportEnvKeyDataCreate = { +export type TImportEnvKeyDataDTO = { decryptionKey: string; encryptedJson: { nonce: string; data: string }; - actor: ActorType; - actorId: string; - actorOrgId: string; - actorAuthMethod: ActorAuthMethod; -}; +} & Omit; + +export type TImportVaultDataDTO = { + vaultAccessToken: string; + vaultNamespace: string; + mappingType: VaultMappingType; + vaultUrl: string; +} & Omit; export type TImportInfisicalDataCreate = { data: InfisicalImportData; diff --git a/frontend/src/hooks/api/migration/index.ts b/frontend/src/hooks/api/migration/index.ts new file mode 100644 index 000000000..f8dd99d03 --- /dev/null +++ b/frontend/src/hooks/api/migration/index.ts @@ -0,0 +1 @@ +export * from "./mutations"; diff --git a/frontend/src/hooks/api/migration/mutations.tsx b/frontend/src/hooks/api/migration/mutations.tsx index 87ba89eab..51c0f4910 100644 --- a/frontend/src/hooks/api/migration/mutations.tsx +++ b/frontend/src/hooks/api/migration/mutations.tsx @@ -15,7 +15,7 @@ export const useImportEnvKey = () => { formData.append("file", file); try { - const response = await apiRequest.post("/api/v3/migrate/env-key/", formData, { + const response = await apiRequest.post("/api/v3/external-migration/env-key/", formData, { headers: { "Content-Type": "multipart/form-data" }, @@ -39,3 +39,23 @@ export const useImportEnvKey = () => { } }); }; + +export const useImportVault = () => { + return useMutation({ + mutationFn: async ({ + vaultAccessToken, + vaultNamespace, + vaultUrl + }: { + vaultAccessToken: string; + vaultNamespace: string; + vaultUrl: string; + }) => { + await apiRequest.post("/api/v3/external-migration/vault/", { + vaultAccessToken, + vaultNamespace, + vaultUrl + }); + } + }); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx index 06631ffe1..f596a957d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; -import { faKey } from "@fortawesome/free-solid-svg-icons"; +import { faKey, faVault } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AnimatePresence, motion } from "framer-motion"; import { Modal, ModalContent } from "@app/components/v2"; import { EnvKeyPlatformModal } from "./EnvKeyPlatformModal"; +import { VaultPlatformModal } from "./VaultPlatformModal copy"; type Props = { isOpen?: boolean; @@ -22,6 +23,11 @@ const PLATFORM_LIST = [ icon: faKey, platform: "env-key", title: "Env Key" + }, + { + icon: faVault, + platform: "vault", + title: "Vault" } ] as const; @@ -82,18 +88,32 @@ export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => { )} - {wizardStep === WizardSteps.PlatformInputs && - selectedPlatform?.platform === "env-key" && ( - - handleFormReset(false)} /> - - )} + {wizardStep === WizardSteps.PlatformInputs && ( + <> + {selectedPlatform?.platform === "env-key" && ( + + handleFormReset(false)} /> + + )} + {selectedPlatform?.platform === "vault" && ( + + handleFormReset(false)} /> + + )} + + )} diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx new file mode 100644 index 000000000..3d9755a28 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx @@ -0,0 +1,226 @@ +import { useRef } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Tooltip } from "@app/components/v2"; +import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; +import { useImportVault } from "@app/hooks/api/migration/mutations"; + +type Props = { + id?: string; + onClose: () => void; +}; + +enum VaultMappingType { + Namespace = "namespace", + KeyVault = "key-vault" +} + +const MAPPING_TYPE_MENU_ITEMS = [ + { + value: VaultMappingType.Namespace, + label: "Namespaces", + tooltip: ( +
+ When using namespaces for mapping, each namespace within Vault will be created in Infisical + as a project. Each key vault (KV) inside the namespace, will be created as an environment + inside the corresponding project. +
+
Namespace → Project
+
Key Vault → Project Environment
+
Secret Path → Secret Folder
+
Secret data → Secrets
+
+
+ ) + }, + { + value: VaultMappingType.KeyVault, + label: "Key Vaults", + tooltip: ( +
+ When using key vaults for mapping, each key vault within Vault will be created in Infisical + as a project. Each secret path inside the key vault, will be created as an environment + inside the corresponding project. When using Key Vaults as the mapping type, a default + environment called "Production" will be created for each project, which will + contain the secrets from the key vault. +
+
Key Vault → Project
+
Default Environment (Production)
+
Secret Path → Secret Folder
+
Secret data → Secrets
+
+
+ ) + } +]; + +export const VaultPlatformModal = ({ onClose }: Props) => { + const formSchema = z.object({ + vaultUrl: z.string().min(1), + vaultNamespace: z.string().min(1), + vaultAccessToken: z.string().min(1), + mappingType: z.nativeEnum(VaultMappingType) + }); + type TFormData = z.infer; + + const fileUploadRef = useRef(null); + + const { mutateAsync: importVault } = useImportVault(); + + const { + control, + handleSubmit, + reset, + formState: { isLoading, isDirty, isSubmitting, isValid } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + const onSubmit = async (data: TFormData) => { + try { + await importVault({ + vaultAccessToken: data.vaultAccessToken, + vaultNamespace: data.vaultNamespace, + vaultUrl: data.vaultUrl + }); + createNotification({ + title: "Import started", + text: "Your data is being imported. You will receive an email when the import is complete or if the import fails. This may take up to 10 minutes.", + type: "info" + }); + + onClose(); + reset(); + + if (fileUploadRef.current) { + fileUploadRef.current.value = ""; + } + } catch { + reset(); + } + }; + + return ( +
+ +

+ The Vault migration currently supports importing static secrets from Vault + Dedicated/Self-Hosted. Namespaces are treated as projects, Secret Engines are treated as + environments, and secret paths are treated as folders. +

+ Currently only KV Secret Engine V2 is supported for Vault migrations. +
+

+
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + +
+ {MAPPING_TYPE_MENU_ITEMS.map((el) => ( +
field.onChange(el.value)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") { + field.onChange(el.value); + } + }} + > +
+
{el.label}
+ {el.tooltip && ( +
+ + + +
+ )} +
+
+ ))} +
+
+ )} + /> + +
+ + +
+ +
+ ); +}; From 185cc4efba94153c634355c6d02ea82b0f168a59 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 22 Jul 2025 01:50:28 +0400 Subject: [PATCH 02/79] Update VaultPlatformModal copy.tsx --- .../components/VaultPlatformModal copy.tsx | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx index 3d9755a28..eee9055df 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx @@ -17,11 +17,30 @@ type Props = { }; enum VaultMappingType { - Namespace = "namespace", - KeyVault = "key-vault" + KeyVault = "key-vault", + Namespace = "namespace" } const MAPPING_TYPE_MENU_ITEMS = [ + { + value: VaultMappingType.KeyVault, + label: "Key Vaults", + tooltip: ( +
+ When using key vaults for mapping, each key vault within Vault will be created in Infisical + as a project. Each secret path inside the key vault, will be created as an environment + inside the corresponding project. When using Key Vaults as the project mapping type, a + default environment called "Production" will be created for each project, which + will contain the secrets from the key vault. +
+
Key Vault → Project
+
Default Environment (Production)
+
Secret Path → Secret Folder
+
Secret data → Secrets
+
+
+ ) + }, { value: VaultMappingType.Namespace, label: "Namespaces", @@ -38,25 +57,6 @@ const MAPPING_TYPE_MENU_ITEMS = [ ) - }, - { - value: VaultMappingType.KeyVault, - label: "Key Vaults", - tooltip: ( -
- When using key vaults for mapping, each key vault within Vault will be created in Infisical - as a project. Each secret path inside the key vault, will be created as an environment - inside the corresponding project. When using Key Vaults as the mapping type, a default - environment called "Production" will be created for each project, which will - contain the secrets from the key vault. -
-
Key Vault → Project
-
Default Environment (Production)
-
Secret Path → Secret Folder
-
Secret data → Secrets
-
-
- ) } ]; @@ -65,7 +65,7 @@ export const VaultPlatformModal = ({ onClose }: Props) => { vaultUrl: z.string().min(1), vaultNamespace: z.string().min(1), vaultAccessToken: z.string().min(1), - mappingType: z.nativeEnum(VaultMappingType) + mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault) }); type TFormData = z.infer; @@ -111,8 +111,7 @@ export const VaultPlatformModal = ({ onClose }: Props) => {

The Vault migration currently supports importing static secrets from Vault - Dedicated/Self-Hosted. Namespaces are treated as projects, Secret Engines are treated as - environments, and secret paths are treated as folders. + Dedicated/Self-Hosted.

Currently only KV Secret Engine V2 is supported for Vault migrations.
@@ -164,11 +163,12 @@ export const VaultPlatformModal = ({ onClose }: Props) => { ( From bfd8b648717aebca6f2c6c596fb61a05e53f234a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 22 Jul 2025 02:15:21 +0400 Subject: [PATCH 03/79] requested changes --- .../src/server/routes/v3/external-migration-router.ts | 6 +++--- frontend/src/hooks/api/migration/mutations.tsx | 7 +++++-- .../components/SelectImportFromPlatformModal.tsx | 2 +- ...aultPlatformModal copy.tsx => VaultPlatformModal.tsx} | 9 ++------- 4 files changed, 11 insertions(+), 13 deletions(-) rename frontend/src/pages/organization/SettingsPage/components/ImportTab/components/{VaultPlatformModal copy.tsx => VaultPlatformModal.tsx} (97%) diff --git a/backend/src/server/routes/v3/external-migration-router.ts b/backend/src/server/routes/v3/external-migration-router.ts index d7aca92e2..0681a8bc9 100644 --- a/backend/src/server/routes/v3/external-migration-router.ts +++ b/backend/src/server/routes/v3/external-migration-router.ts @@ -2,7 +2,7 @@ import fastifyMultipart from "@fastify/multipart"; import { z } from "zod"; import { BadRequestError } from "@app/lib/errors"; -import { readLimit } from "@app/server/config/rateLimiter"; +import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { VaultMappingType } from "@app/services/external-migration/external-migration-types"; @@ -17,7 +17,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider bodyLimit: MB25_IN_BYTES, url: "/env-key", config: { - rateLimit: readLimit + rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { @@ -59,7 +59,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider method: "POST", url: "/vault", config: { - rateLimit: readLimit + rateLimit: writeLimit }, schema: { body: z.object({ diff --git a/frontend/src/hooks/api/migration/mutations.tsx b/frontend/src/hooks/api/migration/mutations.tsx index 51c0f4910..86ab2377c 100644 --- a/frontend/src/hooks/api/migration/mutations.tsx +++ b/frontend/src/hooks/api/migration/mutations.tsx @@ -45,16 +45,19 @@ export const useImportVault = () => { mutationFn: async ({ vaultAccessToken, vaultNamespace, - vaultUrl + vaultUrl, + mappingType }: { vaultAccessToken: string; vaultNamespace: string; vaultUrl: string; + mappingType: string; }) => { await apiRequest.post("/api/v3/external-migration/vault/", { vaultAccessToken, vaultNamespace, - vaultUrl + vaultUrl, + mappingType }); } }); diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx index f596a957d..6da5a44c9 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/SelectImportFromPlatformModal.tsx @@ -6,7 +6,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Modal, ModalContent } from "@app/components/v2"; import { EnvKeyPlatformModal } from "./EnvKeyPlatformModal"; -import { VaultPlatformModal } from "./VaultPlatformModal copy"; +import { VaultPlatformModal } from "./VaultPlatformModal"; type Props = { isOpen?: boolean; diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal.tsx similarity index 97% rename from frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx rename to frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal.tsx index eee9055df..6037933ae 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal copy.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal.tsx @@ -69,8 +69,6 @@ export const VaultPlatformModal = ({ onClose }: Props) => { }); type TFormData = z.infer; - const fileUploadRef = useRef(null); - const { mutateAsync: importVault } = useImportVault(); const { @@ -87,7 +85,8 @@ export const VaultPlatformModal = ({ onClose }: Props) => { await importVault({ vaultAccessToken: data.vaultAccessToken, vaultNamespace: data.vaultNamespace, - vaultUrl: data.vaultUrl + vaultUrl: data.vaultUrl, + mappingType: data.mappingType }); createNotification({ title: "Import started", @@ -97,10 +96,6 @@ export const VaultPlatformModal = ({ onClose }: Props) => { onClose(); reset(); - - if (fileUploadRef.current) { - fileUploadRef.current.value = ""; - } } catch { reset(); } From 464e32b0e95d5b434106a52f443bdc0361836bea Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 22 Jul 2025 13:04:00 +0400 Subject: [PATCH 04/79] Update VaultPlatformModal.tsx --- .../components/ImportTab/components/VaultPlatformModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal.tsx index 6037933ae..eddeade31 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ImportTab/components/VaultPlatformModal.tsx @@ -1,4 +1,3 @@ -import { useRef } from "react"; import { Controller, useForm } from "react-hook-form"; import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; From e76e0f7bcc62b6374550f892fe14e923accd4daa Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 22 Jul 2025 17:14:45 -0700 Subject: [PATCH 05/79] improvement: improve dashboard filter behavior and design --- .../OverviewPage/OverviewPage.tsx | 103 +++++++++-------- .../SecretDashboardPage.tsx | 36 ++++-- .../components/ActionBar/ActionBar.tsx | 106 +++++++++++------- 3 files changed, 145 insertions(+), 100 deletions(-) diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index f58edbd33..70863c2a9 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -11,12 +11,12 @@ import { faArrowRightToBracket, faArrowUp, faFileImport, + faFilter, faFingerprint, faFolder, faFolderBlank, faFolderPlus, faKey, - faList, faPlus, faRotate } from "@fortawesome/free-solid-svg-icons"; @@ -96,7 +96,12 @@ import { OrderByDirection } from "@app/hooks/api/generic/types"; import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries"; import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types"; import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; -import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types"; +import { + SecretType, + SecretV3RawSanitized, + TSecretFolder, + WorkspaceEnv +} from "@app/hooks/api/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { useDynamicSecretOverview, @@ -144,11 +149,11 @@ type Filter = { }; const DEFAULT_FILTER_STATE = { - [RowType.Folder]: true, - [RowType.DynamicSecret]: true, - [RowType.Secret]: true, - [RowType.Import]: true, - [RowType.SecretRotation]: true + [RowType.Folder]: false, + [RowType.DynamicSecret]: false, + [RowType.Secret]: false, + [RowType.Import]: false, + [RowType.SecretRotation]: false }; const DEFAULT_COLLAPSED_HEADER_HEIGHT = 120; @@ -265,11 +270,8 @@ export const OverviewPage = () => { ) ); - const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs); - - useEffect(() => { - setVisibleEnvs(userAvailableEnvs); - }, [userAvailableEnvs]); + const [filteredEnvs, setFilteredEnvs] = useState([]); + const visibleEnvs = filteredEnvs.length ? filteredEnvs : userAvailableEnvs; const { secretImports, @@ -282,6 +284,7 @@ export const OverviewPage = () => { environments: (userAvailableEnvs || []).map(({ slug }) => slug) }); + const isFilteredByResources = Object.values(filter).some(Boolean); const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview( { projectId: workspaceId, @@ -289,11 +292,11 @@ export const OverviewPage = () => { secretPath, orderDirection, orderBy, - includeFolders: filter.folder, - includeDynamicSecrets: filter.dynamic, - includeSecrets: filter.secret, - includeImports: filter.import, - includeSecretRotations: filter.rotation, + includeFolders: isFilteredByResources ? filter.folder : true, + includeDynamicSecrets: isFilteredByResources ? filter.dynamic : true, + includeSecrets: isFilteredByResources ? filter.secret : true, + includeImports: isFilteredByResources ? filter.import : true, + includeSecretRotations: isFilteredByResources ? filter.rotation : true, search: debouncedSearchFilter, limit, offset @@ -529,10 +532,10 @@ export const OverviewPage = () => { }; const handleEnvSelect = (envId: string) => { - if (visibleEnvs.map((env) => env.id).includes(envId)) { - setVisibleEnvs(visibleEnvs.filter((env) => env.id !== envId)); + if (filteredEnvs.map((env) => env.id).includes(envId)) { + setFilteredEnvs(filteredEnvs.filter((env) => env.id !== envId)); } else { - setVisibleEnvs(visibleEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId))); + setFilteredEnvs(filteredEnvs.concat(userAvailableEnvs.filter((env) => env.id === envId))); } }; @@ -792,11 +795,11 @@ export const OverviewPage = () => { envNames: string[], envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[] ): ProjectSecretsImportedBy[] => { - const filteredEnvs = envs.filter((env) => envNames.includes(env.environment)); + const environments = envs.filter((env) => envNames.includes(env.environment)); - if (filteredEnvs.length === 0) return []; + if (environments.length === 0) return []; - const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy); + const allImportedBy = environments.flatMap((env) => env.importedBy); const groupedBySlug: Record = {}; allImportedBy.forEach((item) => { @@ -902,9 +905,7 @@ export const OverviewPage = () => { const isTableEmpty = totalCount === 0; - const isTableFiltered = - Boolean(Object.values(filter).filter((enabled) => !enabled).length) || - userAvailableEnvs.length !== visibleEnvs.length; + const isTableFiltered = isFilteredByResources || filteredEnvs.length > 0; if (!isProjectV3) return ( @@ -969,26 +970,42 @@ export const OverviewPage = () => {
+ {isTableFiltered && ( + + )} {userAvailableEnvs.length > 0 && ( - + } > - - - - + Filters + {/*
- Choose visible environments + Filter by Environment {userAvailableEnvs.map((availableEnv) => { const { id: envId, name } = availableEnv; - const isEnvSelected = visibleEnvs.map((env) => env.id).includes(envId); + const isEnvSelected = filteredEnvs.map((env) => env.id).includes(envId); return ( { @@ -1082,7 +1099,6 @@ export const OverviewPage = () => { handleEnvSelect(envId); }} key={envId} - disabled={visibleEnvs?.length === 1} icon={isEnvSelected && } iconPos="right" > @@ -1405,13 +1421,6 @@ export const OverviewPage = () => { className="bg-mineshaft-700" /> )} - {userAvailableEnvs.length > 0 && visibleEnvs.length === 0 && ( - - - - - - )} {userAvailableEnvs.length === 0 && ( @@ -1439,12 +1448,12 @@ export const OverviewPage = () => { )} - {isTableEmpty && !isOverviewLoading && visibleEnvs.length > 0 && ( + {isTableEmpty && !isOverviewLoading && isTableFiltered && ( { searchFilter: (routerQueryParams.search as string) || "", // these should always be on by default for the UI, they will be disabled for the query below based off permissions include: { - [RowType.Folder]: true, - [RowType.Import]: true, - [RowType.DynamicSecret]: true, - [RowType.Secret]: true, - [RowType.SecretRotation]: true + [RowType.Folder]: false, + [RowType.Import]: false, + [RowType.DynamicSecret]: false, + [RowType.Secret]: false, + [RowType.SecretRotation]: false } }; @@ -242,6 +242,7 @@ const Page = () => { } }, [currentWorkspace, environment]); + const isResourceTypeFiltered = Object.values(filter.include).some(Boolean); const { data, isPending: isDetailsLoading, @@ -255,12 +256,14 @@ const Page = () => { orderBy, search: debouncedSearchFilter, orderDirection, - includeImports: canReadSecretImports && filter.include.import, - includeFolders: filter.include.folder, + includeImports: canReadSecretImports && (isResourceTypeFiltered ? filter.include.import : true), + includeFolders: isResourceTypeFiltered ? filter.include.folder : true, viewSecretValue: canReadSecretValue, - includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, - includeSecrets: canReadSecret && filter.include.secret, - includeSecretRotations: canReadSecretRotations && filter.include.rotation, + includeDynamicSecrets: + canReadDynamicSecret && (isResourceTypeFiltered ? filter.include.dynamic : true), + includeSecrets: canReadSecret && (isResourceTypeFiltered ? filter.include.secret : true), + includeSecretRotations: + canReadSecretRotations && (isResourceTypeFiltered ? filter.include.rotation : true), tags: filter.tags }); @@ -769,6 +772,19 @@ const Page = () => { isPITEnabled={isPITEnabled} hasPathPolicies={hasPathPolicies} onRequestAccess={(params) => handlePopUpOpen("requestAccess", params)} + onClearFilters={() => + setFilter((prev) => ({ + ...prev, + tags: {}, + include: { + secret: false, + import: false, + dynamic: false, + rotation: false, + folder: false + } + })) + } />
void; hasPathPolicies: boolean; + onClearFilters: () => void; }; export const ActionBar = ({ @@ -159,7 +160,8 @@ export const ActionBar = ({ isPITEnabled = false, usedBySecretSyncs, onRequestAccess, - hasPathPolicies + hasPathPolicies, + onClearFilters }: Props) => { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "addFolder", @@ -661,6 +663,9 @@ export const ActionBar = ({ } }; + const isTableFiltered = + Object.values(filter.tags).some(Boolean) || Object.values(filter.include).some(Boolean); + return ( <>
@@ -676,18 +681,22 @@ export const ActionBar = ({
- !include).length) && - "border-primary/50 text-primary" + "flex h-[2.5rem]", + isTableFiltered && "border-primary/40 bg-primary/10" )} + leftIcon={ + + } > - - + Filters + Filter By @@ -762,51 +771,62 @@ export const ActionBar = ({ Secrets
- - } - > - Tags - - - - Apply Tags to Filter Secrets - - {tags.map(({ id, slug, color }) => ( - { - evt.preventDefault(); - onToggleTagFilter(slug); - }} - key={id} - icon={filter?.tags[slug] && } - iconPos="right" - > -
-
- {slug} -
- - ))} - - + {Boolean(tags.length) && ( + + } + > + Tags + + + + Apply Tags to Filter Secrets + + {tags.map(({ id, slug, color }) => ( + { + evt.preventDefault(); + onToggleTagFilter(slug); + }} + key={id} + icon={filter?.tags[slug] && } + iconPos="right" + > +
+
+ {slug} +
+ + ))} + + + )}
+ {isTableFiltered && ( + + )} +
{isProtectedBranch && ( - + )}
-
From 05408bc15156f273bb8252df96376d66e8f85f64 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Wed, 23 Jul 2025 09:54:41 -0300 Subject: [PATCH 06/79] Allow multiple environments on secret and access policies --- backend/src/@types/knex.d.ts | 21 +++ ...2152841_add-policies-environments-table.ts | 93 ++++++++++ .../access-approval-policies-environments.ts | 25 +++ backend/src/db/schemas/models.ts | 2 + .../secret-approval-policies-environments.ts | 25 +++ .../v1/access-approval-policy-router.ts | 107 +++++++----- .../v1/secret-approval-policy-router.ts | 72 ++++---- .../access-approval-policy-dal.ts | 159 ++++++++++++++++-- .../access-approval-policy-environment-dal.ts | 31 ++++ .../access-approval-policy-service.ts | 106 ++++++++---- .../access-approval-policy-types.ts | 36 +++- .../access-approval-request-dal.ts | 19 ++- .../access-approval-request-service.ts | 37 +++- .../secret-approval-policy-dal.ts | 105 +++++++++++- .../secret-approval-policy-environment-dal.ts | 31 ++++ .../secret-approval-policy-service.ts | 95 ++++++++--- .../secret-approval-policy-types.ts | 4 +- .../secret-approval-request-dal.ts | 9 +- .../secret-approval-request-service.ts | 5 + backend/src/server/routes/index.ts | 10 +- backend/src/server/routes/sanitizedSchemas.ts | 7 + .../project-env/project-env-service.ts | 22 ++- .../src/hooks/api/accessApproval/mutation.tsx | 10 +- .../src/hooks/api/accessApproval/types.ts | 6 +- .../src/hooks/api/secretApproval/mutation.tsx | 10 +- .../src/hooks/api/secretApproval/types.ts | 5 +- frontend/src/hooks/usePathAccessPolicies.tsx | 3 +- .../SpecificPrivilegeSection.tsx | 4 +- .../ApprovalPolicyList/ApprovalPolicyList.tsx | 24 ++- .../components/AccessPolicyModal.tsx | 29 ++-- .../components/ApprovalPolicyRow.tsx | 4 +- 31 files changed, 918 insertions(+), 198 deletions(-) create mode 100644 backend/src/db/migrations/20250722152841_add-policies-environments-table.ts create mode 100644 backend/src/db/schemas/access-approval-policies-environments.ts create mode 100644 backend/src/db/schemas/secret-approval-policies-environments.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts create mode 100644 backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 7ead9f84b..6f3e3029d 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -489,6 +489,11 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; +import { + TAccessApprovalPoliciesEnvironments, + TAccessApprovalPoliciesEnvironmentsInsert, + TAccessApprovalPoliciesEnvironmentsUpdate +} from "@app/db/schemas/access-approval-policies-environments"; import { TIdentityLdapAuths, TIdentityLdapAuthsInsert, @@ -504,6 +509,11 @@ import { TProjectMicrosoftTeamsConfigsInsert, TProjectMicrosoftTeamsConfigsUpdate } from "@app/db/schemas/project-microsoft-teams-configs"; +import { + TSecretApprovalPoliciesEnvironments, + TSecretApprovalPoliciesEnvironmentsInsert, + TSecretApprovalPoliciesEnvironmentsUpdate +} from "@app/db/schemas/secret-approval-policies-environments"; import { TSecretReminderRecipients, TSecretReminderRecipientsInsert, @@ -881,6 +891,12 @@ declare module "knex/types/tables" { TAccessApprovalPoliciesBypassersUpdate >; + [TableName.AccessApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType< + TAccessApprovalPoliciesEnvironments, + TAccessApprovalPoliciesEnvironmentsInsert, + TAccessApprovalPoliciesEnvironmentsUpdate + >; + [TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType< TAccessApprovalRequests, TAccessApprovalRequestsInsert, @@ -929,6 +945,11 @@ declare module "knex/types/tables" { TSecretApprovalRequestSecretTagsInsert, TSecretApprovalRequestSecretTagsUpdate >; + [TableName.SecretApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType< + TSecretApprovalPoliciesEnvironments, + TSecretApprovalPoliciesEnvironmentsInsert, + TSecretApprovalPoliciesEnvironmentsUpdate + >; [TableName.SecretRotation]: KnexOriginal.CompositeTableType< TSecretRotations, TSecretRotationsInsert, diff --git a/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts new file mode 100644 index 000000000..3c2edc365 --- /dev/null +++ b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts @@ -0,0 +1,93 @@ +import { Knex } from "knex"; + +import { selectAllTableCols } from "@app/lib/knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyEnvironment))) { + await knex.schema.createTable(TableName.AccessApprovalPolicyEnvironment, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("policyId").notNullable(); + t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); + t.uuid("envId").notNullable(); + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + if (!(await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment))) { + await knex.schema.createTable(TableName.SecretApprovalPolicyEnvironment, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("policyId").notNullable(); + t.foreign("policyId").references("id").inTable(TableName.SecretApprovalPolicy).onDelete("CASCADE"); + t.uuid("envId").notNullable(); + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.dropForeign(["envId"]); + + // Add the new foreign key constraint with ON DELETE SET NULL + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL"); + }); + + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.dropForeign(["envId"]); + + // Add the new foreign key constraint with ON DELETE SET NULL + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL"); + }); + + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment); + await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment); + + const existingAccessApprovalPolicies = await knex(TableName.AccessApprovalPolicy) + .select(selectAllTableCols(TableName.AccessApprovalPolicy)) + .whereNotNull(`${TableName.AccessApprovalPolicy}.envId`); + + const accessApprovalPolicies = existingAccessApprovalPolicies.map(async (policy) => { + await knex(TableName.AccessApprovalPolicyEnvironment).insert({ + policyId: policy.id, + envId: policy.envId + }); + }); + + await Promise.all(accessApprovalPolicies); + + const existingSecretApprovalPolicies = await knex(TableName.SecretApprovalPolicy) + .select(selectAllTableCols(TableName.SecretApprovalPolicy)) + .whereNotNull(`${TableName.SecretApprovalPolicy}.envId`); + + const secretApprovalPolicies = existingSecretApprovalPolicies.map(async (policy) => { + await knex(TableName.SecretApprovalPolicyEnvironment).insert({ + policyId: policy.id, + envId: policy.envId + }); + }); + + await Promise.all(secretApprovalPolicies); +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.AccessApprovalPolicyEnvironment)) { + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyEnvironment); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment); + } + if (await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment)) { + await knex.schema.dropTableIfExists(TableName.SecretApprovalPolicyEnvironment); + await dropOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment); + } + + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.dropForeign(["envId"]); + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + }); + + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.dropForeign(["envId"]); + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + }); +} diff --git a/backend/src/db/schemas/access-approval-policies-environments.ts b/backend/src/db/schemas/access-approval-policies-environments.ts new file mode 100644 index 000000000..fa2a859c2 --- /dev/null +++ b/backend/src/db/schemas/access-approval-policies-environments.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalPoliciesEnvironmentsSchema = z.object({ + id: z.string().uuid(), + policyId: z.string().uuid(), + envId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalPoliciesEnvironments = z.infer; +export type TAccessApprovalPoliciesEnvironmentsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TAccessApprovalPoliciesEnvironmentsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 75d36833b..8ea73cdf4 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -100,6 +100,7 @@ export enum TableName { AccessApprovalPolicyBypasser = "access_approval_policies_bypassers", AccessApprovalRequest = "access_approval_requests", AccessApprovalRequestReviewer = "access_approval_requests_reviewers", + AccessApprovalPolicyEnvironment = "access_approval_policies_environments", SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers", @@ -107,6 +108,7 @@ export enum TableName { SecretApprovalRequestReviewer = "secret_approval_requests_reviewers", SecretApprovalRequestSecret = "secret_approval_requests_secrets", SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags", + SecretApprovalPolicyEnvironment = "secret_approval_policies_environments", SecretRotation = "secret_rotations", SecretRotationOutput = "secret_rotation_outputs", SamlConfig = "saml_configs", diff --git a/backend/src/db/schemas/secret-approval-policies-environments.ts b/backend/src/db/schemas/secret-approval-policies-environments.ts new file mode 100644 index 000000000..0420fe75d --- /dev/null +++ b/backend/src/db/schemas/secret-approval-policies-environments.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SecretApprovalPoliciesEnvironmentsSchema = z.object({ + id: z.string().uuid(), + policyId: z.string().uuid(), + envId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TSecretApprovalPoliciesEnvironments = z.infer; +export type TSecretApprovalPoliciesEnvironmentsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TSecretApprovalPoliciesEnvironmentsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index 177f5e1fd..c01d2fc28 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -17,52 +17,66 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi rateLimit: writeLimit }, schema: { - body: z.object({ - projectSlug: z.string().trim(), - name: z.string().optional(), - secretPath: z.string().trim().min(1, { message: "Secret path cannot be empty" }).transform(removeTrailingSlash), - environment: z.string(), - approvers: z - .discriminatedUnion("type", [ - z.object({ - type: z.literal(ApproverType.Group), - id: z.string(), - sequence: z.number().int().default(1) - }), - z.object({ - type: z.literal(ApproverType.User), - id: z.string().optional(), - username: z.string().optional(), - sequence: z.number().int().default(1) + body: z + .object({ + projectSlug: z.string().trim(), + name: z.string().optional(), + secretPath: z + .string() + .trim() + .min(1, { message: "Secret path cannot be empty" }) + .transform(removeTrailingSlash), + environment: z.string().optional(), + environments: z.string().array().optional(), + approvers: z + .discriminatedUnion("type", [ + z.object({ + type: z.literal(ApproverType.Group), + id: z.string(), + sequence: z.number().int().default(1) + }), + z.object({ + type: z.literal(ApproverType.User), + id: z.string().optional(), + username: z.string().optional(), + sequence: z.number().int().default(1) + }) + ]) + .array() + .max(100, "Cannot have more than 100 approvers") + .min(1, { message: "At least one approver should be provided" }) + .refine( + // @ts-expect-error this is ok + (el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)), + "Must provide either username or id" + ), + bypassers: z + .discriminatedUnion("type", [ + z.object({ type: z.literal(BypasserType.Group), id: z.string() }), + z.object({ + type: z.literal(BypasserType.User), + id: z.string().optional(), + username: z.string().optional() + }) + ]) + .array() + .max(100, "Cannot have more than 100 bypassers") + .optional(), + approvalsRequired: z + .object({ + numberOfApprovals: z.number().int(), + stepNumber: z.number().int() }) - ]) - .array() - .max(100, "Cannot have more than 100 approvers") - .min(1, { message: "At least one approver should be provided" }) - .refine( - // @ts-expect-error this is ok - (el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)), - "Must provide either username or id" - ), - bypassers: z - .discriminatedUnion("type", [ - z.object({ type: z.literal(BypasserType.Group), id: z.string() }), - z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() }) - ]) - .array() - .max(100, "Cannot have more than 100 bypassers") - .optional(), - approvalsRequired: z - .object({ - numberOfApprovals: z.number().int(), - stepNumber: z.number().int() - }) - .array() - .optional(), - approvals: z.number().min(1).default(1), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), - allowedSelfApprovals: z.boolean().default(true) - }), + .array() + .optional(), + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) + }) + .refine( + (val) => Boolean(val.environment) || Boolean(val.environments), + "Must provide either environment or environments" + ), response: { 200: z.object({ approval: sapPubSchema @@ -78,7 +92,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, ...req.body, projectSlug: req.body.projectSlug, - name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, + name: req.body.name ?? `${req.body.environment || req.body.environments?.join("-")}-${nanoid(3)}`, enforcementLevel: req.body.enforcementLevel }); return { approval }; @@ -211,6 +225,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi approvals: z.number().min(1).optional(), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), allowedSelfApprovals: z.boolean().default(true), + environments: z.array(z.string()).optional(), approvalsRequired: z .object({ numberOfApprovals: z.number().int(), diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index 46b2544b2..dc87b83f2 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -17,34 +17,45 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi rateLimit: writeLimit }, schema: { - body: z.object({ - workspaceId: z.string(), - name: z.string().optional(), - environment: z.string(), - secretPath: z - .string() - .min(1, { message: "Secret path cannot be empty" }) - .transform((val) => removeTrailingSlash(val)), - approvers: z - .discriminatedUnion("type", [ - z.object({ type: z.literal(ApproverType.Group), id: z.string() }), - z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() }) - ]) - .array() - .min(1, { message: "At least one approver should be provided" }) - .max(100, "Cannot have more than 100 approvers"), - bypassers: z - .discriminatedUnion("type", [ - z.object({ type: z.literal(BypasserType.Group), id: z.string() }), - z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() }) - ]) - .array() - .max(100, "Cannot have more than 100 bypassers") - .optional(), - approvals: z.number().min(1).default(1), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), - allowedSelfApprovals: z.boolean().default(true) - }), + body: z + .object({ + workspaceId: z.string(), + name: z.string().optional(), + environment: z.string().optional(), + environments: z.string().array().optional(), + secretPath: z + .string() + .min(1, { message: "Secret path cannot be empty" }) + .transform((val) => removeTrailingSlash(val)), + approvers: z + .discriminatedUnion("type", [ + z.object({ type: z.literal(ApproverType.Group), id: z.string() }), + z.object({ + type: z.literal(ApproverType.User), + id: z.string().optional(), + username: z.string().optional() + }) + ]) + .array() + .min(1, { message: "At least one approver should be provided" }) + .max(100, "Cannot have more than 100 approvers"), + bypassers: z + .discriminatedUnion("type", [ + z.object({ type: z.literal(BypasserType.Group), id: z.string() }), + z.object({ + type: z.literal(BypasserType.User), + id: z.string().optional(), + username: z.string().optional() + }) + ]) + .array() + .max(100, "Cannot have more than 100 bypassers") + .optional(), + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) + }) + .refine((data) => data.environment || data.environments, "At least one environment should be provided"), response: { 200: z.object({ approval: sapPubSchema @@ -60,7 +71,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, projectId: req.body.workspaceId, ...req.body, - name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, + name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`, enforcementLevel: req.body.enforcementLevel }); return { approval }; @@ -103,7 +114,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .optional() .transform((val) => (val ? removeTrailingSlash(val) : undefined)), enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), - allowedSelfApprovals: z.boolean().default(true) + allowedSelfApprovals: z.boolean().default(true), + environments: z.array(z.string()).optional() }), response: { 200: z.object({ diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 995534f8f..e01b51a5d 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -26,6 +26,7 @@ export interface TAccessApprovalPolicyDALFactory >, customFilter?: { policyId?: string; + envId?: string; }, tx?: Knex ) => Promise< @@ -55,11 +56,6 @@ export interface TAccessApprovalPolicyDALFactory allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; - environment: { - id: string; - name: string; - slug: string; - }; projectId: string; bypassers: ( | { @@ -72,6 +68,11 @@ export interface TAccessApprovalPolicyDALFactory type: BypasserType.Group; } )[]; + environments: { + id: string; + name: string; + slug: string; + }[]; }[] >; findById: ( @@ -95,11 +96,11 @@ export interface TAccessApprovalPolicyDALFactory allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; - environment: { + environments: { id: string; name: string; slug: string; - }; + }[]; projectId: string; } | undefined @@ -143,6 +144,26 @@ export interface TAccessApprovalPolicyDALFactory } | undefined >; + findPoliciesByEnvIdAndSecretPath: ( + { envIds, secretPath }: { envIds: string[]; secretPath: string }, + tx?: Knex + ) => Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + approvals: number; + enforcementLevel: string; + allowedSelfApprovals: boolean; + secretPath: string; + deletedAt?: Date | null | undefined; + environments: { + id: string; + name: string; + slug: string; + }[]; + projectId: string; + }>; } export interface TAccessApprovalPolicyServiceFactory { @@ -367,6 +388,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo filter: TFindFilter, customFilter?: { policyId?: string; + envId?: string; } ) => { const result = await tx(TableName.AccessApprovalPolicy) @@ -377,7 +399,17 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId); } }) - .join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .where((qb) => { + if (customFilter?.envId) { + void qb.where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", customFilter.envId); + } + }) + .join( + TableName.AccessApprovalPolicyEnvironment, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyEnvironment}.policyId` + ) + .join(TableName.Environment, `${TableName.AccessApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`) .leftJoin( TableName.AccessApprovalPolicyApprover, `${TableName.AccessApprovalPolicy}.id`, @@ -404,7 +436,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo .select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser)) .select(tx.ref("name").withSchema(TableName.Environment).as("envName")) .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) - .select(tx.ref("id").withSchema(TableName.Environment).as("envId")) + .select(tx.ref("id").withSchema(TableName.Environment).as("environmentId")) .select(tx.ref("projectId").withSchema(TableName.Environment)) .select(selectAllTableCols(TableName.AccessApprovalPolicy)); @@ -448,6 +480,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo sequence: approverSequence, approvalsRequired }) + }, + { + key: "environmentId", + label: "environments" as const, + mapper: ({ environmentId: id, envName, envSlug }) => ({ + id, + name: envName, + slug: envSlug + }) } ] }); @@ -470,11 +511,6 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo data: docs, key: "id", parentMapper: (data) => ({ - environment: { - id: data.envId, - name: data.envName, - slug: data.envSlug - }, projectId: data.projectId, ...AccessApprovalPoliciesSchema.parse(data) // secretPath: data.secretPath || undefined, @@ -517,6 +553,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo id, type: BypasserType.Group as const }) + }, + { + key: "environmentId", + label: "environments" as const, + mapper: ({ environmentId: id, envName, envSlug }) => ({ + id, + name: envName, + slug: envSlug + }) } ] }); @@ -545,14 +590,20 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo // eslint-disable-next-line @typescript-eslint/no-misused-promises buildFindFilter( { - envId, secretPath }, TableName.AccessApprovalPolicy ) ) + .join( + TableName.AccessApprovalPolicyEnvironment, + `${TableName.AccessApprovalPolicyEnvironment}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + .where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", envId) .orderBy("deletedAt", "desc") .orderByRaw(`"deletedAt" IS NULL`) + .select(selectAllTableCols(TableName.AccessApprovalPolicy)) .first(); return result; @@ -561,5 +612,81 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo } }; - return { ...accessApprovalPolicyOrm, find, findById, softDeleteById, findLastValidPolicy }; + const findPoliciesByEnvIdAndSecretPath: TAccessApprovalPolicyDALFactory["findPoliciesByEnvIdAndSecretPath"] = async ( + { envIds, secretPath }, + tx + ) => { + try { + const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicy) + .join( + TableName.AccessApprovalPolicyEnvironment, + `${TableName.AccessApprovalPolicyEnvironment}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + .join( + TableName.Environment, + `${TableName.AccessApprovalPolicyEnvironment}.envId`, + `${TableName.Environment}.id` + ) + .where( + // eslint-disable-next-line @typescript-eslint/no-misused-promises + buildFindFilter( + { + $in: { + envId: envIds + } + }, + TableName.AccessApprovalPolicyEnvironment + ) + ) + .where( + // eslint-disable-next-line @typescript-eslint/no-misused-promises + buildFindFilter( + { + secretPath + }, + TableName.AccessApprovalPolicy + ) + ) + .whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`) + .orderBy("deletedAt", "desc") + .orderByRaw(`"deletedAt" IS NULL`) + .select(selectAllTableCols(TableName.AccessApprovalPolicy)) + .select(db.ref("name").withSchema(TableName.Environment).as("envName")) + .select(db.ref("slug").withSchema(TableName.Environment).as("envSlug")) + .select(db.ref("id").withSchema(TableName.Environment).as("environmentId")) + .select(db.ref("projectId").withSchema(TableName.Environment)); + const formattedDocs = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (data) => ({ + projectId: data.projectId, + ...AccessApprovalPoliciesSchema.parse(data) + }), + childrenMapper: [ + { + key: "environmentId", + label: "environments" as const, + mapper: ({ environmentId: id, envName, envSlug }) => ({ + id, + name: envName, + slug: envSlug + }) + } + ] + }); + return formattedDocs?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "FindPoliciesByEnvIdAndSecretPath" }); + } + }; + + return { + ...accessApprovalPolicyOrm, + find, + findById, + softDeleteById, + findLastValidPolicy, + findPoliciesByEnvIdAndSecretPath + }; }; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts new file mode 100644 index 000000000..8485df036 --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TAccessApprovalPolicyEnvironmentDALFactory = ReturnType; + +export const accessApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => { + const accessApprovalPolicyEnvironmentOrm = ormify(db, TableName.AccessApprovalPolicyEnvironment); + + const findAvailablePoliciesIds = async (envId: string, tx?: Knex) => { + try { + const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicyEnvironment) + .join( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalPolicyEnvironment}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + .where({ [`${TableName.AccessApprovalPolicyEnvironment}.envId` as "envId"]: envId }) + .whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`) + .select(selectAllTableCols(TableName.AccessApprovalPolicyEnvironment)); + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "findAvailablePoliciesIds" }); + } + }; + + return { ...accessApprovalPolicyEnvironmentOrm, findAvailablePoliciesIds }; +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index fa487d0b7..693198f0e 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -20,6 +20,7 @@ import { TAccessApprovalPolicyBypasserDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; +import { TAccessApprovalPolicyEnvironmentDALFactory } from "./access-approval-policy-environment-dal"; import { ApproverType, BypasserType, @@ -44,12 +45,14 @@ type TAccessApprovalPolicyServiceFactoryDep = { additionalPrivilegeDAL: Pick; accessApprovalRequestReviewerDAL: Pick; orgMembershipDAL: Pick; + accessApprovalPolicyEnvironmentDAL: TAccessApprovalPolicyEnvironmentDALFactory; }; export const accessApprovalPolicyServiceFactory = ({ accessApprovalPolicyDAL, accessApprovalPolicyApproverDAL, accessApprovalPolicyBypasserDAL, + accessApprovalPolicyEnvironmentDAL, groupDAL, permissionService, projectEnvDAL, @@ -62,21 +65,22 @@ export const accessApprovalPolicyServiceFactory = ({ }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { const $policyExists = async ({ envId, + envIds, secretPath, policyId }: { - envId: string; + envId?: string; + envIds?: string[]; secretPath: string; policyId?: string; }) => { - const policy = await accessApprovalPolicyDAL - .findOne({ - envId, - secretPath, - deletedAt: null - }) - .catch(() => null); - + if (!envId && !envIds) { + throw new BadRequestError({ message: "Must provide either envId or envIds" }); + } + const policy = await accessApprovalPolicyDAL.findPoliciesByEnvIdAndSecretPath({ + secretPath, + envIds: envId ? [envId] : envIds || [] + }); return policyId ? policy && policy.id !== policyId : Boolean(policy); }; @@ -92,6 +96,7 @@ export const accessApprovalPolicyServiceFactory = ({ bypassers, projectSlug, environment, + environments, enforcementLevel, allowedSelfApprovals, approvalsRequired @@ -123,13 +128,23 @@ export const accessApprovalPolicyServiceFactory = ({ ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval ); - const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); - if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` }); + const mergedEnvs = (environment ? [environment] : environments) || []; + if (mergedEnvs.length === 0) { + throw new BadRequestError({ message: "Must provide either environment or environments" }); + } + const envs = await projectEnvDAL.find({ $in: { slug: mergedEnvs }, projectId: project.id }); + if (!envs.length || envs.length !== mergedEnvs.length) { + const notFoundEnvs = mergedEnvs.filter((env) => !envs.find((el) => el.slug === env)); + throw new NotFoundError({ message: `One or more environments not found: ${notFoundEnvs.join(", ")}` }); + } - if (await $policyExists({ envId: env.id, secretPath })) { - throw new BadRequestError({ - message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'` - }); + for (const env of envs) { + // eslint-disable-next-line no-await-in-loop + if (await $policyExists({ envId: env.id, secretPath })) { + throw new BadRequestError({ + message: `A policy for secret path '${secretPath}' already exists in environment '${env.slug}'` + }); + } } let approverUserIds = userApprovers; @@ -197,7 +212,7 @@ export const accessApprovalPolicyServiceFactory = ({ const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const doc = await accessApprovalPolicyDAL.create( { - envId: env.id, + envId: envs[0].id, approvals, secretPath, name, @@ -206,6 +221,10 @@ export const accessApprovalPolicyServiceFactory = ({ }, tx ); + await accessApprovalPolicyEnvironmentDAL.insertMany( + envs.map((el) => ({ policyId: doc.id, envId: el.id })), + tx + ); if (approverUserIds.length) { await accessApprovalPolicyApproverDAL.insertMany( @@ -258,7 +277,7 @@ export const accessApprovalPolicyServiceFactory = ({ return doc; }); - return { ...accessApproval, environment: env, projectId: project.id }; + return { ...accessApproval, environments: envs, projectId: project.id, environment: envs[0] }; }; const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] = @@ -276,7 +295,10 @@ export const accessApprovalPolicyServiceFactory = ({ }); const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); - return accessApprovalPolicies; + return accessApprovalPolicies.map((policy) => ({ + ...policy, + environment: policy.environments[0] + })); }; const updateAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["updateAccessApprovalPolicy"] = async ({ @@ -292,7 +314,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvals, enforcementLevel, allowedSelfApprovals, - approvalsRequired + approvalsRequired, + environments }: TUpdateAccessApprovalPolicy) => { const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group); @@ -320,15 +343,23 @@ export const accessApprovalPolicyServiceFactory = ({ throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); } + let envs = accessApprovalPolicy.environments; + if ( + environments && + (environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env))) + ) { + envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: accessApprovalPolicy.projectId }); + } + if ( await $policyExists({ - envId: accessApprovalPolicy.envId, + envIds: envs.map((env) => env.id), secretPath: secretPath || accessApprovalPolicy.secretPath, policyId: accessApprovalPolicy.id }) ) { throw new BadRequestError({ - message: `A policy for secret path '${secretPath}' already exists in environment '${accessApprovalPolicy.environment.slug}'` + message: `A policy for secret path '${secretPath}' already exists` }); } @@ -484,6 +515,14 @@ export const accessApprovalPolicyServiceFactory = ({ ); } + if (environments) { + await accessApprovalPolicyEnvironmentDAL.delete({ policyId: doc.id }, tx); + await accessApprovalPolicyEnvironmentDAL.insertMany( + envs.map((env) => ({ policyId: doc.id, envId: env.id })), + tx + ); + } + await accessApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx); if (bypasserUserIds.length) { @@ -513,7 +552,8 @@ export const accessApprovalPolicyServiceFactory = ({ return { ...updatedPolicy, - environment: accessApprovalPolicy.environment, + environments: accessApprovalPolicy.environments, + environment: accessApprovalPolicy.environments[0], projectId: accessApprovalPolicy.projectId }; }; @@ -563,7 +603,10 @@ export const accessApprovalPolicyServiceFactory = ({ } }); - return policy; + return { + ...policy, + environment: policy.environments[0] + }; }; const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({ @@ -592,11 +635,13 @@ export const accessApprovalPolicyServiceFactory = ({ const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` }); - const policies = await accessApprovalPolicyDAL.find({ - envId: environment.id, - projectId: project.id, - deletedAt: null - }); + const policies = await accessApprovalPolicyDAL.find( + { + projectId: project.id, + deletedAt: null + }, + { envId: environment.id } + ); if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` }); return { count: policies.length }; @@ -627,7 +672,10 @@ export const accessApprovalPolicyServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); - return policy; + return { + ...policy, + environment: policy.environments[0] + }; }; return { diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index f3f195914..27ec228f7 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -26,7 +26,8 @@ export enum BypasserType { export type TCreateAccessApprovalPolicy = { approvals: number; secretPath: string; - environment: string; + environment?: string; + environments?: string[]; approvers: ( | { type: ApproverType.Group; id: string; sequence?: number } | { type: ApproverType.User; id?: string; username?: string; sequence?: number } @@ -58,6 +59,7 @@ export type TUpdateAccessApprovalPolicy = { enforcementLevel?: EnforcementLevel; allowedSelfApprovals: boolean; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; + environments?: string[]; } & Omit; export type TDeleteAccessApprovalPolicy = { @@ -113,6 +115,15 @@ export interface TAccessApprovalPolicyServiceFactory { slug: string; position: number; }; + environments: { + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + projectId: string; + slug: string; + position: number; + }[]; projectId: string; name: string; id: string; @@ -153,6 +164,11 @@ export interface TAccessApprovalPolicyServiceFactory { name: string; slug: string; }; + environments: { + id: string; + name: string; + slug: string; + }[]; projectId: string; }>; updateAccessApprovalPolicy: ({ @@ -168,13 +184,19 @@ export interface TAccessApprovalPolicyServiceFactory { approvals, enforcementLevel, allowedSelfApprovals, - approvalsRequired + approvalsRequired, + environments }: TUpdateAccessApprovalPolicy) => Promise<{ environment: { id: string; name: string; slug: string; }; + environments: { + id: string; + name: string; + slug: string; + }[]; projectId: string; name: string; id: string; @@ -225,6 +247,11 @@ export interface TAccessApprovalPolicyServiceFactory { name: string; slug: string; }; + environments: { + id: string; + name: string; + slug: string; + }[]; projectId: string; bypassers: ( | { @@ -276,6 +303,11 @@ export interface TAccessApprovalPolicyServiceFactory { name: string; slug: string; }; + environments: { + id: string; + name: string; + slug: string; + }[]; projectId: string; bypassers: ( | { diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index 671d2c1de..9872df067 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -65,7 +65,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit environment } ] }); diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 8b823ee91..03dd9e7de 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -86,6 +86,25 @@ export const accessApprovalRequestServiceFactory = ({ projectMicrosoftTeamsConfigDAL, projectSlackConfigDAL }: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => { + const $getEnvironmentFromPermissions = (permissions: unknown): string | null => { + if (!Array.isArray(permissions) || permissions.length === 0) { + return null; + } + + const firstPermission = permissions[0] as unknown[]; + if (!Array.isArray(firstPermission) || firstPermission.length < 3) { + return null; + } + + const metadata = firstPermission[2] as Record; + if (typeof metadata === "object" && metadata !== null && "environment" in metadata) { + const env = metadata.environment; + return typeof env === "string" ? env : null; + } + + return null; + }; + const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({ isTemporary, temporaryRange, @@ -323,13 +342,27 @@ export const accessApprovalRequestServiceFactory = ({ throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` }); } - const { policy, environment } = accessApprovalRequest; + const { policy, environments, permissions } = accessApprovalRequest; if (policy.deletedAt) { throw new BadRequestError({ message: "The policy associated with this access request has been deleted." }); } + const permissionEnvironment = $getEnvironmentFromPermissions(permissions); + if ( + !permissionEnvironment || + (!environments.includes(permissionEnvironment) && status === ApprovalStatus.APPROVED) + ) { + throw new BadRequestError({ + message: `The original policy ${policy.name} is not attached to environment '${permissionEnvironment}'.` + }); + } + const environment = await projectEnvDAL.findOne({ + projectId: accessApprovalRequest.projectId, + slug: permissionEnvironment + }); + const { membership, hasRole } = await permissionService.getProjectPermission({ actor, actorId, @@ -550,7 +583,7 @@ export const accessApprovalRequestServiceFactory = ({ requesterEmail: actingUser.email, bypassReason: bypassReason || "No reason provided", secretPath: policy.secretPath || "/", - environment, + environment: environment?.name || permissionEnvironment, approvalUrl: `${cfg.SITE_URL}/projects/${project.id}/secret-manager/approval`, requestType: "access" }, diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts index fd8be93cf..c19286105 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts @@ -23,6 +23,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { filter: TFindFilter, customFilter?: { sapId?: string; + envId?: string; } ) => tx(TableName.SecretApprovalPolicy) @@ -33,7 +34,17 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId); } }) - .join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .join( + TableName.SecretApprovalPolicyEnvironment, + `${TableName.SecretApprovalPolicyEnvironment}.policyId`, + `${TableName.SecretApprovalPolicy}.id` + ) + .join(TableName.Environment, `${TableName.SecretApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`) + .where((qb) => { + if (customFilter?.envId) { + void qb.where(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", customFilter.envId); + } + }) .leftJoin( TableName.SecretApprovalPolicyApprover, `${TableName.SecretApprovalPolicy}.id`, @@ -97,7 +108,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { .select( tx.ref("name").withSchema(TableName.Environment).as("envName"), tx.ref("slug").withSchema(TableName.Environment).as("envSlug"), - tx.ref("id").withSchema(TableName.Environment).as("envId"), + tx.ref("id").withSchema(TableName.Environment).as("environmentId"), tx.ref("projectId").withSchema(TableName.Environment) ) .select(selectAllTableCols(TableName.SecretApprovalPolicy)) @@ -146,6 +157,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { firstName, lastName }) + }, + { + key: "environmentId", + label: "environments" as const, + mapper: ({ environmentId, envName, envSlug }) => ({ + id: environmentId, + name: envName, + slug: envSlug + }) } ] }); @@ -160,6 +180,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { filter: TFindFilter, customFilter?: { sapId?: string; + envId?: string; }, tx?: Knex ) => { @@ -221,6 +242,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { mapper: ({ approverGroupUserId: userId }) => ({ userId }) + }, + { + key: "environmentId", + label: "environments" as const, + mapper: ({ environmentId, envName, envSlug }) => ({ + id: environmentId, + name: envName, + slug: envSlug + }) } ] }); @@ -235,5 +265,74 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { return softDeletedPolicy; }; - return { ...secretApprovalPolicyOrm, findById, find, softDeleteById }; + const findPoliciesByEnvIdAndSecretPath = async ( + { envIds, secretPath }: { envIds: string[]; secretPath: string }, + tx?: Knex + ) => { + try { + const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicy) + .join( + TableName.SecretApprovalPolicyEnvironment, + `${TableName.SecretApprovalPolicyEnvironment}.policyId`, + `${TableName.SecretApprovalPolicy}.id` + ) + .join( + TableName.Environment, + `${TableName.SecretApprovalPolicyEnvironment}.envId`, + `${TableName.Environment}.id` + ) + .where( + // eslint-disable-next-line @typescript-eslint/no-misused-promises + buildFindFilter( + { + $in: { + envId: envIds + } + }, + TableName.SecretApprovalPolicyEnvironment + ) + ) + .where( + // eslint-disable-next-line @typescript-eslint/no-misused-promises + buildFindFilter( + { + secretPath + }, + TableName.SecretApprovalPolicy + ) + ) + .whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`) + .orderBy("deletedAt", "desc") + .orderByRaw(`"deletedAt" IS NULL`) + .select(selectAllTableCols(TableName.SecretApprovalPolicy)) + .select(db.ref("name").withSchema(TableName.Environment).as("envName")) + .select(db.ref("slug").withSchema(TableName.Environment).as("envSlug")) + .select(db.ref("id").withSchema(TableName.Environment).as("environmentId")) + .select(db.ref("projectId").withSchema(TableName.Environment)); + const formattedDocs = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (data) => ({ + projectId: data.projectId, + ...SecretApprovalPoliciesSchema.parse(data) + }), + childrenMapper: [ + { + key: "environmentId", + label: "environments" as const, + mapper: ({ environmentId: id, envName, envSlug }) => ({ + id, + name: envName, + slug: envSlug + }) + } + ] + }); + return formattedDocs?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "FindPoliciesByEnvIdAndSecretPath" }); + } + }; + + return { ...secretApprovalPolicyOrm, findById, find, softDeleteById, findPoliciesByEnvIdAndSecretPath }; }; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts new file mode 100644 index 000000000..58c2173de --- /dev/null +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts @@ -0,0 +1,31 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TSecretApprovalPolicyEnvironmentDALFactory = ReturnType; + +export const secretApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => { + const secretApprovalPolicyEnvironmentOrm = ormify(db, TableName.SecretApprovalPolicyEnvironment); + + const findAvailablePoliciesIds = async (envId: string, tx?: Knex) => { + try { + const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicyEnvironment) + .join( + TableName.SecretApprovalPolicy, + `${TableName.SecretApprovalPolicyEnvironment}.policyId`, + `${TableName.SecretApprovalPolicy}.id` + ) + .where({ [`${TableName.SecretApprovalPolicyEnvironment}.envId` as "envId"]: envId }) + .whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`) + .select(selectAllTableCols(TableName.SecretApprovalPolicyEnvironment)); + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "findAvailablePoliciesIds" }); + } + }; + + return { ...secretApprovalPolicyEnvironmentOrm, findAvailablePoliciesIds }; +}; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 80127c071..b6392d018 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -18,6 +18,7 @@ import { TSecretApprovalPolicyBypasserDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; +import { TSecretApprovalPolicyEnvironmentDALFactory } from "./secret-approval-policy-environment-dal"; import { TCreateSapDTO, TDeleteSapDTO, @@ -35,12 +36,13 @@ const getPolicyScore = (policy: { secretPath?: string | null }) => type TSecretApprovalPolicyServiceFactoryDep = { permissionService: Pick; secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory; - projectEnvDAL: Pick; + projectEnvDAL: Pick; userDAL: Pick; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory; licenseService: Pick; secretApprovalRequestDAL: Pick; + secretApprovalPolicyEnvironmentDAL: TSecretApprovalPolicyEnvironmentDALFactory; }; export type TSecretApprovalPolicyServiceFactory = ReturnType; @@ -50,27 +52,30 @@ export const secretApprovalPolicyServiceFactory = ({ permissionService, secretApprovalPolicyApproverDAL, secretApprovalPolicyBypasserDAL, + secretApprovalPolicyEnvironmentDAL, projectEnvDAL, userDAL, licenseService, secretApprovalRequestDAL }: TSecretApprovalPolicyServiceFactoryDep) => { const $policyExists = async ({ + envIds, envId, secretPath, policyId }: { - envId: string; + envIds?: string[]; + envId?: string; secretPath: string; policyId?: string; }) => { - const policy = await secretApprovalPolicyDAL - .findOne({ - envId, - secretPath, - deletedAt: null - }) - .catch(() => null); + if (!envIds && !envId) { + throw new BadRequestError({ message: "At least one environment should be provided" }); + } + const policy = await secretApprovalPolicyDAL.findPoliciesByEnvIdAndSecretPath({ + envIds: envId ? [envId] : envIds || [], + secretPath + }); return policyId ? policy && policy.id !== policyId : Boolean(policy); }; @@ -87,6 +92,7 @@ export const secretApprovalPolicyServiceFactory = ({ projectId, secretPath, environment, + environments, enforcementLevel, allowedSelfApprovals }: TCreateSapDTO) => { @@ -125,17 +131,23 @@ export const secretApprovalPolicyServiceFactory = ({ }); } - const env = await projectEnvDAL.findOne({ slug: environment, projectId }); - if (!env) { - throw new NotFoundError({ - message: `Environment with slug '${environment}' not found in project with ID ${projectId}` - }); + const mergedEnvs = (environment ? [environment] : environments) || []; + if (mergedEnvs.length === 0) { + throw new BadRequestError({ message: "Must provide either environment or environments" }); + } + const envs = await projectEnvDAL.find({ $in: { slug: mergedEnvs }, projectId }); + if (!envs.length || envs.length !== mergedEnvs.length) { + const notFoundEnvs = mergedEnvs.filter((env) => !envs.find((el) => el.slug === env)); + throw new NotFoundError({ message: `One or more environments not found: ${notFoundEnvs.join(", ")}` }); } - if (await $policyExists({ envId: env.id, secretPath })) { - throw new BadRequestError({ - message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'` - }); + for (const env of envs) { + // eslint-disable-next-line no-await-in-loop + if (await $policyExists({ envId: env.id, secretPath })) { + throw new BadRequestError({ + message: `A policy for secret path '${secretPath}' already exists in environment '${env.slug}'` + }); + } } let groupBypassers: string[] = []; @@ -179,7 +191,7 @@ export const secretApprovalPolicyServiceFactory = ({ const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => { const doc = await secretApprovalPolicyDAL.create( { - envId: env.id, + envId: envs[0].id, approvals, secretPath, name, @@ -188,6 +200,13 @@ export const secretApprovalPolicyServiceFactory = ({ }, tx ); + await secretApprovalPolicyEnvironmentDAL.insertMany( + envs.map((env) => ({ + envId: env.id, + policyId: doc.id + })), + tx + ); let userApproverIds = userApprovers; if (userApproverNames.length) { @@ -251,12 +270,13 @@ export const secretApprovalPolicyServiceFactory = ({ return doc; }); - return { ...secretApproval, environment: env, projectId }; + return { ...secretApproval, environments: envs, projectId, environment: envs[0] }; }; const updateSecretApprovalPolicy = async ({ approvers, bypassers, + environments, secretPath, name, actorId, @@ -286,16 +306,22 @@ export const secretApprovalPolicyServiceFactory = ({ message: `Secret approval policy with ID '${secretPolicyId}' not found` }); } - + let envs = secretApprovalPolicy.environments; + if ( + environments && + (environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env))) + ) { + envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: secretApprovalPolicy.projectId }); + } if ( await $policyExists({ - envId: secretApprovalPolicy.envId, + envIds: envs.map((env) => env.id), secretPath: secretPath || secretApprovalPolicy.secretPath, policyId: secretApprovalPolicy.id }) ) { throw new BadRequestError({ - message: `A policy for secret path '${secretPath}' already exists in environment '${secretApprovalPolicy.environment.slug}'` + message: `A policy for secret path '${secretPath}' already exists` }); } @@ -412,6 +438,17 @@ export const secretApprovalPolicyServiceFactory = ({ ); } + if (environments) { + await secretApprovalPolicyEnvironmentDAL.delete({ policyId: doc.id }, tx); + await secretApprovalPolicyEnvironmentDAL.insertMany( + envs.map((env) => ({ + envId: env.id, + policyId: doc.id + })), + tx + ); + } + await secretApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx); if (bypasserUserIds.length) { @@ -438,7 +475,8 @@ export const secretApprovalPolicyServiceFactory = ({ }); return { ...updatedSap, - environment: secretApprovalPolicy.environment, + environments: secretApprovalPolicy.environments, + environment: secretApprovalPolicy.environments[0], projectId: secretApprovalPolicy.projectId }; }; @@ -483,7 +521,12 @@ export const secretApprovalPolicyServiceFactory = ({ const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx); return updatedPolicy; }); - return { ...deletedPolicy, projectId: sapPolicy.projectId, environment: sapPolicy.environment }; + return { + ...deletedPolicy, + projectId: sapPolicy.projectId, + environments: sapPolicy.environments, + environment: sapPolicy.environments[0] + }; }; const getSecretApprovalPolicyByProjectId = async ({ @@ -515,7 +558,7 @@ export const secretApprovalPolicyServiceFactory = ({ }); } - const policies = await secretApprovalPolicyDAL.find({ envId: env.id, deletedAt: null }); + const policies = await secretApprovalPolicyDAL.find({ deletedAt: null }, { envId: env.id }); if (!policies.length) return; // this will filter policies either without scoped to secret path or the one that matches with secret path const policiesFilteredByPath = policies.filter( diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index ba5334e5c..80369e638 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -5,7 +5,8 @@ import { ApproverType, BypasserType } from "../access-approval-policy/access-app export type TCreateSapDTO = { approvals: number; secretPath: string; - environment: string; + environment?: string; + environments?: string[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; bypassers?: ( | { type: BypasserType.Group; id: string } @@ -29,6 +30,7 @@ export type TUpdateSapDTO = { name?: string; enforcementLevel?: EnforcementLevel; allowedSelfApprovals?: boolean; + environments?: string[]; } & Omit; export type TDeleteSapDTO = { diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index c098d9b31..49f31bdf6 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -40,6 +40,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalPolicy}.id` ) + .leftJoin(TableName.SecretApprovalPolicyEnvironment, (bd) => { + bd.on( + `${TableName.SecretApprovalPolicy}.id`, + "=", + `${TableName.SecretApprovalPolicyEnvironment}.policyId` + ).andOn(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", `${TableName.SecretFolder}.envId`); + }) .leftJoin( db(TableName.Users).as("statusChangedByUser"), `${TableName.SecretApprovalRequest}.statusChangedByUserId`, @@ -146,7 +153,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("projectId").withSchema(TableName.Environment), tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), - tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"), + tx.ref("envId").withSchema(TableName.SecretApprovalPolicyEnvironment).as("policyEnvId"), tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index f7c1d4b1b..75efa948c 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -531,6 +531,11 @@ export const secretApprovalRequestServiceFactory = ({ message: "The policy associated with this secret approval request has been deleted." }); } + if (!policy.envId) { + throw new BadRequestError({ + message: "The policy associated with this secret approval request is not linked to the environment." + }); + } const { hasRole } = await permissionService.getProjectPermission({ actor: ActorType.USER, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index a1e336844..f3f79260f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -11,6 +11,7 @@ import { accessApprovalPolicyBypasserDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; +import { accessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal"; import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal"; import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal"; @@ -76,6 +77,7 @@ import { secretApprovalPolicyBypasserDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal"; import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal"; +import { secretApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-environment-dal"; import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-reviewer-dal"; @@ -418,9 +420,11 @@ export const registerRoutes = async ( const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); const accessApprovalPolicyBypasserDAL = accessApprovalPolicyBypasserDALFactory(db); const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db); + const accessApprovalPolicyEnvironmentDAL = accessApprovalPolicyEnvironmentDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const sapBypasserDAL = secretApprovalPolicyBypasserDALFactory(db); + const sapEnvironmentDAL = secretApprovalPolicyEnvironmentDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db); @@ -554,6 +558,7 @@ export const registerRoutes = async ( projectEnvDAL, secretApprovalPolicyApproverDAL: sapApproverDAL, secretApprovalPolicyBypasserDAL: sapBypasserDAL, + secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL, permissionService, secretApprovalPolicyDAL, licenseService, @@ -1141,7 +1146,9 @@ export const registerRoutes = async ( keyStore, licenseService, projectDAL, - folderDAL + folderDAL, + accessApprovalPolicyEnvironmentDAL, + secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL }); const projectRoleService = projectRoleServiceFactory({ @@ -1300,6 +1307,7 @@ export const registerRoutes = async ( accessApprovalPolicyDAL, accessApprovalPolicyApproverDAL, accessApprovalPolicyBypasserDAL, + accessApprovalPolicyEnvironmentDAL, groupDAL, permissionService, projectEnvDAL, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index beef663b9..aba8663a3 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -93,6 +93,13 @@ export const sapPubSchema = SecretApprovalPoliciesSchema.merge( name: z.string(), slug: z.string() }), + environments: z.array( + z.object({ + id: z.string(), + name: z.string(), + slug: z.string() + }) + ), projectId: z.string() }) ); diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts index 6773ee600..9d4fb86ee 100644 --- a/backend/src/services/project-env/project-env-service.ts +++ b/backend/src/services/project-env/project-env-service.ts @@ -1,8 +1,10 @@ import { ForbiddenError } from "@casl/ability"; +import { TAccessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { TSecretApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-environment-dal"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; @@ -19,6 +21,8 @@ type TProjectEnvServiceFactoryDep = { permissionService: Pick; licenseService: Pick; keyStore: Pick; + accessApprovalPolicyEnvironmentDAL: Pick; + secretApprovalPolicyEnvironmentDAL: Pick; }; export type TProjectEnvServiceFactory = ReturnType; @@ -29,7 +33,9 @@ export const projectEnvServiceFactory = ({ licenseService, keyStore, projectDAL, - folderDAL + folderDAL, + accessApprovalPolicyEnvironmentDAL, + secretApprovalPolicyEnvironmentDAL }: TProjectEnvServiceFactoryDep) => { const createEnvironment = async ({ projectId, @@ -216,6 +222,20 @@ export const projectEnvServiceFactory = ({ } const env = await projectEnvDAL.transaction(async (tx) => { + const secretApprovalRequest = await secretApprovalPolicyEnvironmentDAL.findAvailablePoliciesIds(id, tx); + if (secretApprovalRequest.length > 0) { + throw new BadRequestError({ + message: "Environment is in use by a secret approval policy", + name: "DeleteEnvironment" + }); + } + const accessApprovalPolicy = await accessApprovalPolicyEnvironmentDAL.findAvailablePoliciesIds(id, tx); + if (accessApprovalPolicy.length > 0) { + throw new BadRequestError({ + message: "Environment is in use by an access approval policy", + name: "DeleteEnvironment" + }); + } const [doc] = await projectEnvDAL.delete({ id, projectId }, tx); if (!doc) throw new NotFoundError({ diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 3b05ff61b..ad7917a8f 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => { return useMutation({ mutationFn: async ({ - environment, + environments, projectSlug, approvals, approvers, @@ -29,7 +29,7 @@ export const useCreateAccessApprovalPolicy = () => { approvalsRequired }) => { const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { - environment, + environments, projectSlug, approvals, bypassers, @@ -63,7 +63,8 @@ export const useUpdateAccessApprovalPolicy = () => { secretPath, enforcementLevel, allowedSelfApprovals, - approvalsRequired + approvalsRequired, + environments }) => { const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { approvals, @@ -73,7 +74,8 @@ export const useUpdateAccessApprovalPolicy = () => { name, enforcementLevel, allowedSelfApprovals, - approvalsRequired + approvalsRequired, + environments }); return data; }, diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 70e9b883e..b07615372 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -10,7 +10,7 @@ export type TAccessApprovalPolicy = { secretPath: string; envId: string; workspace: string; - environment: WorkspaceEnv; + environments: WorkspaceEnv[]; projectId: string; policyType: PolicyType; approversRequired: boolean; @@ -166,7 +166,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = { export type TCreateAccessPolicyDTO = { projectSlug: string; name?: string; - environment: string; + environments: string[]; approvers?: Approver[]; bypassers?: Bypasser[]; approvals?: number; @@ -182,7 +182,7 @@ export type TUpdateAccessPolicyDTO = { approvers?: Approver[]; bypassers?: Bypasser[]; secretPath?: string; - environment?: string; + environments?: string[]; approvals?: number; enforcementLevel?: EnforcementLevel; allowedSelfApprovals: boolean; diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index e2d566e25..8370d0367 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -10,7 +10,7 @@ export const useCreateSecretApprovalPolicy = () => { return useMutation({ mutationFn: async ({ - environment, + environments, workspaceId, approvals, approvers, @@ -21,7 +21,7 @@ export const useCreateSecretApprovalPolicy = () => { allowedSelfApprovals }) => { const { data } = await apiRequest.post("/api/v1/secret-approvals", { - environment, + environments, workspaceId, approvals, approvers, @@ -53,7 +53,8 @@ export const useUpdateSecretApprovalPolicy = () => { secretPath, name, enforcementLevel, - allowedSelfApprovals + allowedSelfApprovals, + environments }) => { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, @@ -62,7 +63,8 @@ export const useUpdateSecretApprovalPolicy = () => { secretPath, name, enforcementLevel, - allowedSelfApprovals + allowedSelfApprovals, + environments }); return data; }, diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index eeb734115..0f0b604f4 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -6,7 +6,7 @@ export type TSecretApprovalPolicy = { workspace: string; name: string; envId: string; - environment: WorkspaceEnv; + environments: WorkspaceEnv[]; secretPath?: string; approvals: number; approvers: Approver[]; @@ -48,7 +48,7 @@ export type TGetSecretApprovalPolicyOfBoardDTO = { export type TCreateSecretPolicyDTO = { workspaceId: string; name?: string; - environment: string; + environments: string[]; secretPath: string; approvers?: Approver[]; bypassers?: Bypasser[]; @@ -68,6 +68,7 @@ export type TUpdateSecretPolicyDTO = { enforcementLevel?: EnforcementLevel; // for invalidating list workspaceId: string; + environments?: string[]; }; export type TDeleteSecretPolicyDTO = { diff --git a/frontend/src/hooks/usePathAccessPolicies.tsx b/frontend/src/hooks/usePathAccessPolicies.tsx index 463e9638d..1fbc5fd52 100644 --- a/frontend/src/hooks/usePathAccessPolicies.tsx +++ b/frontend/src/hooks/usePathAccessPolicies.tsx @@ -49,7 +49,8 @@ export const usePathAccessPolicies = ({ secretPath, environment }: Params) => { return useMemo(() => { const pathPolicies = policies?.filter( (policy) => - policy.environment.slug === environment && matchesPath(secretPath, policy.secretPath) + policy.environments?.some((env) => env.slug === environment) && + matchesPath(secretPath, policy.secretPath) ); return { diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx index 51a83c6e0..0256d3219 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -155,8 +155,8 @@ export const SpecificPrivilegeSecretForm = ({ const selectablePaths = useMemo(() => { if (!policies) return []; - const environmentPolicies = policies.filter( - (policy) => policy.environment.slug === selectedEnvironment + const environmentPolicies = policies.filter((policy) => + policy.environments.find((env) => env.slug === selectedEnvironment) ); privilegeForm.setValue("secretPath", "", { diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx index e84d228cd..3d292f9a2 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx @@ -166,17 +166,20 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => { const filteredPolicies = useMemo( () => policies - .filter(({ policyType, environment, name, secretPath }) => { + .filter(({ policyType, environments, name, secretPath }) => { if (filters.type && policyType !== filters.type) return false; - if (filters.environmentIds.length && !filters.environmentIds.includes(environment.id)) + if ( + filters.environmentIds.length && + !environments.some((env) => filters.environmentIds.includes(env.id)) + ) return false; const searchValue = search.trim().toLowerCase(); return ( name.toLowerCase().includes(searchValue) || - environment.name.toLowerCase().includes(searchValue) || + environments.some((env) => env.name.toLowerCase().includes(searchValue)) || (secretPath ?? "*").toLowerCase().includes(searchValue) ); }) @@ -189,9 +192,18 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => { .toLowerCase() .localeCompare(policyTwo.policyType.toLowerCase()); case PolicyOrderBy.Environment: - return policyOne.environment.name - .toLowerCase() - .localeCompare(policyTwo.environment.name.toLowerCase()); + // eslint-disable-next-line no-case-declarations + const getFirstEnvName = (policy: { environments: { name: string }[] }) => { + if (!policy.environments?.length) return ""; + return ( + policy.environments + .map((env) => env.name?.toLowerCase() || "") + .filter((name) => name) + .sort()[0] || "" + ); + }; + + return getFirstEnvName(policyOne).localeCompare(getFirstEnvName(policyTwo)); case PolicyOrderBy.SecretPath: return (policyOne.secretPath ?? "*") .toLowerCase() diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index 183d8ab1c..e160a88f0 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -54,7 +54,7 @@ type Props = { const formSchema = z .object({ - environment: z.object({ slug: z.string(), name: z.string() }), + environments: z.array(z.object({ slug: z.string(), name: z.string() })).min(1), name: z.string().optional(), secretPath: z.string().trim().min(1), approvals: z.number().min(1).default(1), @@ -134,7 +134,7 @@ const Form = ({ values: editValues ? ({ ...editValues, - environment: editValues.environment, + environments: editValues.environments, userApprovers: editValues?.approvers ?.filter((approver) => approver.type === ApproverType.User) @@ -191,7 +191,7 @@ const Form = ({ const { currentWorkspace } = useWorkspace(); const { data: groups } = useListWorkspaceGroups(projectId); - const environments = currentWorkspace?.environments || []; + const availableEnvironments = currentWorkspace?.environments || []; const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy; const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); @@ -204,11 +204,11 @@ const Form = ({ const formUserBypassers = watch("userBypassers"); const formGroupBypassers = watch("groupBypassers"); - const formEnvironment = watch("environment")?.slug; + const formEnvironments = watch("environments"); const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length; const handleCreatePolicy = async ({ - environment, + environments, groupApprovers, userApprovers, groupBypassers, @@ -226,7 +226,7 @@ const Form = ({ ...data, approvers: [...userApprovers, ...groupApprovers], bypassers: bypassers.length > 0 ? bypassers : undefined, - environment: environment.slug, + environments: environments.map((env) => env.slug), workspaceId: currentWorkspace?.id || "" }); } else { @@ -242,7 +242,7 @@ const Form = ({ numberOfApprovals: el.approvals })), bypassers: bypassers.length > 0 ? bypassers : undefined, - environment: environment.slug, + environments: environments.map((env) => env.slug), projectSlug }); } @@ -261,7 +261,7 @@ const Form = ({ }; const handleUpdatePolicy = async ({ - environment, + environments, userApprovers, groupApprovers, userBypassers, @@ -281,7 +281,8 @@ const Form = ({ ...data, approvers: [...userApprovers, ...groupApprovers], bypassers: bypassers.length > 0 ? bypassers : undefined, - workspaceId: currentWorkspace?.id || "" + workspaceId: currentWorkspace?.id || "", + environments: environments.map((env) => env.slug) }); } else { await updateAccessApprovalPolicy({ @@ -297,7 +298,7 @@ const Form = ({ numberOfApprovals: el.approvals })), bypassers: bypassers.length > 0 ? bypassers : undefined, - environment: environment.slug, + environments: environments.map((env) => env.slug), projectSlug }); } @@ -479,14 +480,14 @@ const Form = ({ )} /> ( option.slug} getOptionLabel={(option) => option.name} /> diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx index 408d718fa..b19ace7ea 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx @@ -37,7 +37,7 @@ import { TWorkspaceUser } from "@app/hooks/api/users/types"; interface IPolicy { id: string; name: string; - environment: WorkspaceEnv; + environments: WorkspaceEnv[]; projectId?: string; secretPath?: string; approvals: number; @@ -112,7 +112,7 @@ export const ApprovalPolicyRow = ({ onClick={() => setIsExpanded.toggle()} > {policy.name || Unnamed Policy} - {policy.environment.name} + {policy.environments.map((env) => env.name).join(", ")} {policy.secretPath || "*"} Date: Wed, 23 Jul 2025 10:37:23 -0300 Subject: [PATCH 07/79] Addressed PR suggestions --- ...22152841_add-policies-environments-table.ts | 2 ++ .../routes/v1/access-approval-policy-router.ts | 3 ++- .../access-approval-policy-dal.ts | 18 +++++++++--------- .../access-approval-policy-environment-dal.ts | 11 ++++++----- .../access-approval-policy-service.ts | 4 ++-- .../secret-approval-policy-dal.ts | 6 +++--- .../secret-approval-policy-environment-dal.ts | 11 ++++++----- .../secret-approval-policy-service.ts | 2 +- .../project-env/project-env-service.ts | 12 ++++++------ frontend/src/hooks/api/accessApproval/types.ts | 1 - frontend/src/hooks/api/secretApproval/types.ts | 1 - 11 files changed, 37 insertions(+), 34 deletions(-) diff --git a/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts index 3c2edc365..450a6c4d6 100644 --- a/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts +++ b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts @@ -14,6 +14,7 @@ export async function up(knex: Knex): Promise { t.uuid("envId").notNullable(); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); + t.unique(["policyId", "envId"]); }); } if (!(await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment))) { @@ -24,6 +25,7 @@ export async function up(knex: Knex): Promise { t.uuid("envId").notNullable(); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); + t.unique(["policyId", "envId"]); }); } diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index c01d2fc28..ef44344de 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -92,7 +92,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, ...req.body, projectSlug: req.body.projectSlug, - name: req.body.name ?? `${req.body.environment || req.body.environments?.join("-")}-${nanoid(3)}`, + name: + req.body.name ?? `${req.body.environment || req.body.environments?.join("-").substring(0, 250)}-${nanoid(3)}`, enforcementLevel: req.body.enforcementLevel }); return { approval }; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index e01b51a5d..9baf762d6 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -144,7 +144,7 @@ export interface TAccessApprovalPolicyDALFactory } | undefined >; - findPoliciesByEnvIdAndSecretPath: ( + findPolicyByEnvIdAndSecretPath: ( { envIds, secretPath }: { envIds: string[]; secretPath: string }, tx?: Knex ) => Promise<{ @@ -399,17 +399,17 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId); } }) - .where((qb) => { - if (customFilter?.envId) { - void qb.where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", customFilter.envId); - } - }) .join( TableName.AccessApprovalPolicyEnvironment, `${TableName.AccessApprovalPolicy}.id`, `${TableName.AccessApprovalPolicyEnvironment}.policyId` ) .join(TableName.Environment, `${TableName.AccessApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`) + .where((qb) => { + if (customFilter?.envId) { + void qb.where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", customFilter.envId); + } + }) .leftJoin( TableName.AccessApprovalPolicyApprover, `${TableName.AccessApprovalPolicy}.id`, @@ -612,7 +612,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo } }; - const findPoliciesByEnvIdAndSecretPath: TAccessApprovalPolicyDALFactory["findPoliciesByEnvIdAndSecretPath"] = async ( + const findPolicyByEnvIdAndSecretPath: TAccessApprovalPolicyDALFactory["findPolicyByEnvIdAndSecretPath"] = async ( { envIds, secretPath }, tx ) => { @@ -677,7 +677,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo }); return formattedDocs?.[0]; } catch (error) { - throw new DatabaseError({ error, name: "FindPoliciesByEnvIdAndSecretPath" }); + throw new DatabaseError({ error, name: "findPolicyByEnvIdAndSecretPath" }); } }; @@ -687,6 +687,6 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo findById, softDeleteById, findLastValidPolicy, - findPoliciesByEnvIdAndSecretPath + findPolicyByEnvIdAndSecretPath }; }; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts index 8485df036..f0d8079cf 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts @@ -3,14 +3,14 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex"; export type TAccessApprovalPolicyEnvironmentDALFactory = ReturnType; export const accessApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => { const accessApprovalPolicyEnvironmentOrm = ormify(db, TableName.AccessApprovalPolicyEnvironment); - const findAvailablePoliciesIds = async (envId: string, tx?: Knex) => { + const findAvailablePoliciesByEnvId = async (envId: string, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicyEnvironment) .join( @@ -18,14 +18,15 @@ export const accessApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => { `${TableName.AccessApprovalPolicyEnvironment}.policyId`, `${TableName.AccessApprovalPolicy}.id` ) - .where({ [`${TableName.AccessApprovalPolicyEnvironment}.envId` as "envId"]: envId }) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter({ envId }, TableName.AccessApprovalPolicyEnvironment)) .whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`) .select(selectAllTableCols(TableName.AccessApprovalPolicyEnvironment)); return docs; } catch (error) { - throw new DatabaseError({ error, name: "findAvailablePoliciesIds" }); + throw new DatabaseError({ error, name: "findAvailablePoliciesByEnvId" }); } }; - return { ...accessApprovalPolicyEnvironmentOrm, findAvailablePoliciesIds }; + return { ...accessApprovalPolicyEnvironmentOrm, findAvailablePoliciesByEnvId }; }; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 693198f0e..0282175df 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -77,9 +77,9 @@ export const accessApprovalPolicyServiceFactory = ({ if (!envId && !envIds) { throw new BadRequestError({ message: "Must provide either envId or envIds" }); } - const policy = await accessApprovalPolicyDAL.findPoliciesByEnvIdAndSecretPath({ + const policy = await accessApprovalPolicyDAL.findPolicyByEnvIdAndSecretPath({ secretPath, - envIds: envId ? [envId] : envIds || [] + envIds: envId ? [envId] : (envIds as string[]) }); return policyId ? policy && policy.id !== policyId : Boolean(policy); }; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts index c19286105..3212fb902 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts @@ -265,7 +265,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { return softDeletedPolicy; }; - const findPoliciesByEnvIdAndSecretPath = async ( + const findPolicyByEnvIdAndSecretPath = async ( { envIds, secretPath }: { envIds: string[]; secretPath: string }, tx?: Knex ) => { @@ -330,9 +330,9 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => { }); return formattedDocs?.[0]; } catch (error) { - throw new DatabaseError({ error, name: "FindPoliciesByEnvIdAndSecretPath" }); + throw new DatabaseError({ error, name: "findPolicyByEnvIdAndSecretPath" }); } }; - return { ...secretApprovalPolicyOrm, findById, find, softDeleteById, findPoliciesByEnvIdAndSecretPath }; + return { ...secretApprovalPolicyOrm, findById, find, softDeleteById, findPolicyByEnvIdAndSecretPath }; }; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts index 58c2173de..d12ace04c 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts @@ -3,14 +3,14 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex"; export type TSecretApprovalPolicyEnvironmentDALFactory = ReturnType; export const secretApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => { const secretApprovalPolicyEnvironmentOrm = ormify(db, TableName.SecretApprovalPolicyEnvironment); - const findAvailablePoliciesIds = async (envId: string, tx?: Knex) => { + const findAvailablePoliciesByEnvId = async (envId: string, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicyEnvironment) .join( @@ -18,14 +18,15 @@ export const secretApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => { `${TableName.SecretApprovalPolicyEnvironment}.policyId`, `${TableName.SecretApprovalPolicy}.id` ) - .where({ [`${TableName.SecretApprovalPolicyEnvironment}.envId` as "envId"]: envId }) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter({ envId }, TableName.SecretApprovalPolicyEnvironment)) .whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`) .select(selectAllTableCols(TableName.SecretApprovalPolicyEnvironment)); return docs; } catch (error) { - throw new DatabaseError({ error, name: "findAvailablePoliciesIds" }); + throw new DatabaseError({ error, name: "findAvailablePoliciesByEnvId" }); } }; - return { ...secretApprovalPolicyEnvironmentOrm, findAvailablePoliciesIds }; + return { ...secretApprovalPolicyEnvironmentOrm, findAvailablePoliciesByEnvId }; }; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index b6392d018..e3d54bcf5 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -72,7 +72,7 @@ export const secretApprovalPolicyServiceFactory = ({ if (!envIds && !envId) { throw new BadRequestError({ message: "At least one environment should be provided" }); } - const policy = await secretApprovalPolicyDAL.findPoliciesByEnvIdAndSecretPath({ + const policy = await secretApprovalPolicyDAL.findPolicyByEnvIdAndSecretPath({ envIds: envId ? [envId] : envIds || [], secretPath }); diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts index 9d4fb86ee..b76e93fed 100644 --- a/backend/src/services/project-env/project-env-service.ts +++ b/backend/src/services/project-env/project-env-service.ts @@ -21,8 +21,8 @@ type TProjectEnvServiceFactoryDep = { permissionService: Pick; licenseService: Pick; keyStore: Pick; - accessApprovalPolicyEnvironmentDAL: Pick; - secretApprovalPolicyEnvironmentDAL: Pick; + accessApprovalPolicyEnvironmentDAL: Pick; + secretApprovalPolicyEnvironmentDAL: Pick; }; export type TProjectEnvServiceFactory = ReturnType; @@ -222,15 +222,15 @@ export const projectEnvServiceFactory = ({ } const env = await projectEnvDAL.transaction(async (tx) => { - const secretApprovalRequest = await secretApprovalPolicyEnvironmentDAL.findAvailablePoliciesIds(id, tx); - if (secretApprovalRequest.length > 0) { + const secretApprovalPolicies = await secretApprovalPolicyEnvironmentDAL.findAvailablePoliciesByEnvId(id, tx); + if (secretApprovalPolicies.length > 0) { throw new BadRequestError({ message: "Environment is in use by a secret approval policy", name: "DeleteEnvironment" }); } - const accessApprovalPolicy = await accessApprovalPolicyEnvironmentDAL.findAvailablePoliciesIds(id, tx); - if (accessApprovalPolicy.length > 0) { + const accessApprovalPolicies = await accessApprovalPolicyEnvironmentDAL.findAvailablePoliciesByEnvId(id, tx); + if (accessApprovalPolicies.length > 0) { throw new BadRequestError({ message: "Environment is in use by an access approval policy", name: "DeleteEnvironment" diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index b07615372..bc569165b 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -8,7 +8,6 @@ export type TAccessApprovalPolicy = { name: string; approvals: number; secretPath: string; - envId: string; workspace: string; environments: WorkspaceEnv[]; projectId: string; diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index 0f0b604f4..8fd86624d 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -5,7 +5,6 @@ export type TSecretApprovalPolicy = { id: string; workspace: string; name: string; - envId: string; environments: WorkspaceEnv[]; secretPath?: string; approvals: number; From e96e7b835d93f1865dd0b292bd7a220f2ea301e7 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 23 Jul 2025 12:43:48 -0700 Subject: [PATCH 08/79] improvements: address feedback --- .../src/server/routes/v1/dashboard-router.ts | 8 +----- .../OverviewPage/OverviewPage.tsx | 27 +++++-------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index c466be087..68be44136 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -270,11 +270,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } }); - - remainingLimit -= imports.length; - adjustedOffset = 0; - } else { - adjustedOffset = Math.max(0, adjustedOffset - totalImportCount); } } @@ -317,7 +312,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } - if (!includeDynamicSecrets && !includeSecrets) + if (!includeDynamicSecrets && !includeSecrets && !includeSecretRotations) return { folders, totalFolderCount, @@ -547,7 +542,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + - (totalImportCount ?? 0) + (totalSecretRotationCount ?? 0) }; } diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 70863c2a9..88f71f8e0 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -10,7 +10,6 @@ import { faArrowRight, faArrowRightToBracket, faArrowUp, - faFileImport, faFilter, faFingerprint, faFolder, @@ -152,7 +151,6 @@ const DEFAULT_FILTER_STATE = { [RowType.Folder]: false, [RowType.DynamicSecret]: false, [RowType.Secret]: false, - [RowType.Import]: false, [RowType.SecretRotation]: false }; @@ -295,7 +293,7 @@ export const OverviewPage = () => { includeFolders: isFilteredByResources ? filter.folder : true, includeDynamicSecrets: isFilteredByResources ? filter.dynamic : true, includeSecrets: isFilteredByResources ? filter.secret : true, - includeImports: isFilteredByResources ? filter.import : true, + includeImports: true, includeSecretRotations: isFilteredByResources ? filter.rotation : true, search: debouncedSearchFilter, limit, @@ -1019,20 +1017,8 @@ export const OverviewPage = () => { Create an environment */} - Filter project resources - { - e.preventDefault(); - handleToggleRowType(RowType.Import); - }} - icon={filter[RowType.Import] && } - iconPos="right" - > -
- - Imports -
-
+ Filter by Resource + { e.preventDefault(); @@ -1448,17 +1434,18 @@ export const OverviewPage = () => { )} - {isTableEmpty && !isOverviewLoading && isTableFiltered && ( + {isTableEmpty && !isOverviewLoading && visibleEnvs.length > 0 && (
@@ -94,7 +101,7 @@ export const LogsSection = withPermission( secretPath: logFilter.secretPath || undefined, secretKey: logFilter.secretKey || undefined, eventMetadata: logFilter?.eventMetadata, - projectId: logFilter?.project?.id, + projectId: project?.id || logFilter?.project?.id, actorType: presets?.actorType, limit: 15, eventType: logFilter?.eventType, @@ -119,7 +126,7 @@ export const LogsSection = withPermission( return (
-
+
{showFilters && ( { {t("common.head-title", { title: t("settings.members.title") })} - + {!isLoading && !serverDetails?.redisConfigured && (

Announcements

diff --git a/frontend/src/pages/project/AuditLogsPage/AuditLogsPage.tsx b/frontend/src/pages/project/AuditLogsPage/AuditLogsPage.tsx new file mode 100644 index 000000000..0c83b0520 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/AuditLogsPage.tsx @@ -0,0 +1,27 @@ +import { Helmet } from "react-helmet"; + +import { PageHeader } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { LogsSection } from "@app/pages/organization/AuditLogsPage/components"; + +export const AuditLogsPage = () => { + const { currentWorkspace } = useWorkspace(); + + return ( +
+ + Project Audit Logs + + +
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/pages/project/AuditLogsPage/route-cert-manager.tsx b/frontend/src/pages/project/AuditLogsPage/route-cert-manager.tsx new file mode 100644 index 000000000..e87c655e0 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-cert-manager.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AuditLogsPage/route-kms.tsx b/frontend/src/pages/project/AuditLogsPage/route-kms.tsx new file mode 100644 index 000000000..7f6883d80 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-kms.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AuditLogsPage/route-secret-manager.tsx b/frontend/src/pages/project/AuditLogsPage/route-secret-manager.tsx new file mode 100644 index 000000000..3483f9270 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-secret-manager.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AuditLogsPage/route-secret-scanning.tsx b/frontend/src/pages/project/AuditLogsPage/route-secret-scanning.tsx new file mode 100644 index 000000000..28d00c5af --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-secret-scanning.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/AuditLogsPage/route-ssh.tsx b/frontend/src/pages/project/AuditLogsPage/route-ssh.tsx new file mode 100644 index 000000000..267e613b7 --- /dev/null +++ b/frontend/src/pages/project/AuditLogsPage/route-ssh.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { AuditLogsPage } from "./AuditLogsPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs" +)({ + component: AuditLogsPage, + beforeLoad: ({ context }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Audit Logs" + } + ] + }; + } +}); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index c9c666a09..0bdf2202d 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -81,10 +81,15 @@ import { Route as secretManagerIntegrationsRouteBitbucketOauthRedirectImport } f import { Route as secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectImport } from './pages/secret-manager/integrations/route-azure-key-vault-oauth-redirect' import { Route as secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectImport } from './pages/secret-manager/integrations/route-azure-app-configurations-oauth-redirect' import { Route as organizationSettingsPageOauthCallbackPageRouteImport } from './pages/organization/SettingsPage/OauthCallbackPage/route' +import { Route as projectAuditLogsPageRouteSshImport } from './pages/project/AuditLogsPage/route-ssh' import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh' +import { Route as projectAuditLogsPageRouteSecretScanningImport } from './pages/project/AuditLogsPage/route-secret-scanning' import { Route as projectAccessControlPageRouteSecretScanningImport } from './pages/project/AccessControlPage/route-secret-scanning' +import { Route as projectAuditLogsPageRouteSecretManagerImport } from './pages/project/AuditLogsPage/route-secret-manager' import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager' +import { Route as projectAuditLogsPageRouteKmsImport } from './pages/project/AuditLogsPage/route-kms' import { Route as projectAccessControlPageRouteKmsImport } from './pages/project/AccessControlPage/route-kms' +import { Route as projectAuditLogsPageRouteCertManagerImport } from './pages/project/AuditLogsPage/route-cert-manager' import { Route as projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager' import { Route as sshSettingsPageRouteImport } from './pages/ssh/SettingsPage/route' import { Route as sshSshHostsPageRouteImport } from './pages/ssh/SshHostsPage/route' @@ -903,6 +908,13 @@ const organizationSettingsPageOauthCallbackPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute, } as any) +const projectAuditLogsPageRouteSshRoute = + projectAuditLogsPageRouteSshImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => sshLayoutRoute, + } as any) + const projectAccessControlPageRouteSshRoute = projectAccessControlPageRouteSshImport.update({ id: '/access-management', @@ -919,6 +931,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecret } as any, ) +const projectAuditLogsPageRouteSecretScanningRoute = + projectAuditLogsPageRouteSecretScanningImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => secretScanningLayoutRoute, + } as any) + const projectAccessControlPageRouteSecretScanningRoute = projectAccessControlPageRouteSecretScanningImport.update({ id: '/access-management', @@ -935,6 +954,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecr } as any, ) +const projectAuditLogsPageRouteSecretManagerRoute = + projectAuditLogsPageRouteSecretManagerImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => secretManagerLayoutRoute, + } as any) + const projectAccessControlPageRouteSecretManagerRoute = projectAccessControlPageRouteSecretManagerImport.update({ id: '/access-management', @@ -942,6 +968,13 @@ const projectAccessControlPageRouteSecretManagerRoute = getParentRoute: () => secretManagerLayoutRoute, } as any) +const projectAuditLogsPageRouteKmsRoute = + projectAuditLogsPageRouteKmsImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => kmsLayoutRoute, + } as any) + const projectAccessControlPageRouteKmsRoute = projectAccessControlPageRouteKmsImport.update({ id: '/access-management', @@ -967,6 +1000,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertMa } as any, ) +const projectAuditLogsPageRouteCertManagerRoute = + projectAuditLogsPageRouteCertManagerImport.update({ + id: '/audit-logs', + path: '/audit-logs', + getParentRoute: () => certManagerLayoutRoute, + } as any) + const projectAccessControlPageRouteCertManagerRoute = projectAccessControlPageRouteCertManagerImport.update({ id: '/access-management', @@ -2722,6 +2762,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/cert-management/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteCertManagerImport + parentRoute: typeof certManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates': { id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates' path: '/certificate-templates' @@ -2743,6 +2790,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteKmsImport parentRoute: typeof kmsLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/kms/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteKmsImport + parentRoute: typeof kmsLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': { id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management' path: '/access-management' @@ -2750,6 +2804,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSecretManagerImport parentRoute: typeof secretManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/secret-management/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteSecretManagerImport + parentRoute: typeof secretManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations': { id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations' path: '/integrations' @@ -2764,6 +2825,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSecretScanningImport parentRoute: typeof secretScanningLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/secret-scanning/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteSecretScanningImport + parentRoute: typeof secretScanningLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources': { id: '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources' path: '/data-sources' @@ -2778,6 +2846,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSshImport parentRoute: typeof sshLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs': { + id: '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs' + path: '/audit-logs' + fullPath: '/projects/ssh/$projectId/audit-logs' + preLoaderRoute: typeof projectAuditLogsPageRouteSshImport + parentRoute: typeof sshLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/': { id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/' path: '/' @@ -3781,6 +3856,7 @@ interface certManagerLayoutRouteChildren { certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute + projectAuditLogsPageRouteCertManagerRoute: typeof projectAuditLogsPageRouteCertManagerRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute @@ -3799,6 +3875,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute, projectAccessControlPageRouteCertManagerRoute: projectAccessControlPageRouteCertManagerRoute, + projectAuditLogsPageRouteCertManagerRoute: + projectAuditLogsPageRouteCertManagerRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRoute: AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRoute: @@ -3839,6 +3917,7 @@ interface kmsLayoutRouteChildren { kmsOverviewPageRouteRoute: typeof kmsOverviewPageRouteRoute kmsSettingsPageRouteRoute: typeof kmsSettingsPageRouteRoute projectAccessControlPageRouteKmsRoute: typeof projectAccessControlPageRouteKmsRoute + projectAuditLogsPageRouteKmsRoute: typeof projectAuditLogsPageRouteKmsRoute projectGroupDetailsByIDPageRouteKmsRoute: typeof projectGroupDetailsByIDPageRouteKmsRoute projectIdentityDetailsByIDPageRouteKmsRoute: typeof projectIdentityDetailsByIDPageRouteKmsRoute projectMemberDetailsByIDPageRouteKmsRoute: typeof projectMemberDetailsByIDPageRouteKmsRoute @@ -3850,6 +3929,7 @@ const kmsLayoutRouteChildren: kmsLayoutRouteChildren = { kmsOverviewPageRouteRoute: kmsOverviewPageRouteRoute, kmsSettingsPageRouteRoute: kmsSettingsPageRouteRoute, projectAccessControlPageRouteKmsRoute: projectAccessControlPageRouteKmsRoute, + projectAuditLogsPageRouteKmsRoute: projectAuditLogsPageRouteKmsRoute, projectGroupDetailsByIDPageRouteKmsRoute: projectGroupDetailsByIDPageRouteKmsRoute, projectIdentityDetailsByIDPageRouteKmsRoute: @@ -4166,6 +4246,7 @@ interface secretManagerLayoutRouteChildren { secretManagerSecretRotationPageRouteRoute: typeof secretManagerSecretRotationPageRouteRoute secretManagerSettingsPageRouteRoute: typeof secretManagerSettingsPageRouteRoute projectAccessControlPageRouteSecretManagerRoute: typeof projectAccessControlPageRouteSecretManagerRoute + projectAuditLogsPageRouteSecretManagerRoute: typeof projectAuditLogsPageRouteSecretManagerRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren secretManagerSecretDashboardPageRouteRoute: typeof secretManagerSecretDashboardPageRouteRoute projectGroupDetailsByIDPageRouteSecretManagerRoute: typeof projectGroupDetailsByIDPageRouteSecretManagerRoute @@ -4186,6 +4267,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = { secretManagerSettingsPageRouteRoute: secretManagerSettingsPageRouteRoute, projectAccessControlPageRouteSecretManagerRoute: projectAccessControlPageRouteSecretManagerRoute, + projectAuditLogsPageRouteSecretManagerRoute: + projectAuditLogsPageRouteSecretManagerRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRoute: AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren, secretManagerSecretDashboardPageRouteRoute: @@ -4241,6 +4324,7 @@ interface secretScanningLayoutRouteChildren { secretScanningSecretScanningFindingsPageRouteRoute: typeof secretScanningSecretScanningFindingsPageRouteRoute secretScanningSettingsPageRouteRoute: typeof secretScanningSettingsPageRouteRoute projectAccessControlPageRouteSecretScanningRoute: typeof projectAccessControlPageRouteSecretScanningRoute + projectAuditLogsPageRouteSecretScanningRoute: typeof projectAuditLogsPageRouteSecretScanningRoute AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren projectGroupDetailsByIDPageRouteSecretScanningRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningRoute projectIdentityDetailsByIDPageRouteSecretScanningRoute: typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute @@ -4254,6 +4338,8 @@ const secretScanningLayoutRouteChildren: secretScanningLayoutRouteChildren = { secretScanningSettingsPageRouteRoute: secretScanningSettingsPageRouteRoute, projectAccessControlPageRouteSecretScanningRoute: projectAccessControlPageRouteSecretScanningRoute, + projectAuditLogsPageRouteSecretScanningRoute: + projectAuditLogsPageRouteSecretScanningRoute, AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren, projectGroupDetailsByIDPageRouteSecretScanningRoute: @@ -4289,6 +4375,7 @@ interface sshLayoutRouteChildren { sshSshHostsPageRouteRoute: typeof sshSshHostsPageRouteRoute sshSettingsPageRouteRoute: typeof sshSettingsPageRouteRoute projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute + projectAuditLogsPageRouteSshRoute: typeof projectAuditLogsPageRouteSshRoute sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute sshSshHostGroupDetailsByIDPageRouteRoute: typeof sshSshHostGroupDetailsByIDPageRouteRoute projectGroupDetailsByIDPageRouteSshRoute: typeof projectGroupDetailsByIDPageRouteSshRoute @@ -4303,6 +4390,7 @@ const sshLayoutRouteChildren: sshLayoutRouteChildren = { sshSshHostsPageRouteRoute: sshSshHostsPageRouteRoute, sshSettingsPageRouteRoute: sshSettingsPageRouteRoute, projectAccessControlPageRouteSshRoute: projectAccessControlPageRouteSshRoute, + projectAuditLogsPageRouteSshRoute: projectAuditLogsPageRouteSshRoute, sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute, sshSshHostGroupDetailsByIDPageRouteRoute: sshSshHostGroupDetailsByIDPageRouteRoute, @@ -4642,14 +4730,19 @@ export interface FileRoutesByFullPath { '/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/projects/cert-management/$projectId/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren '/projects/cert-management/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren '/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute + '/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/projects/secret-management/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/projects/secret-scanning/$projectId/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren '/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute '/projects/cert-management/$projectId/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute '/projects/cert-management/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/projects/secret-management/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute @@ -4852,10 +4945,15 @@ export interface FileRoutesByTo { '/projects/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/projects/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/projects/cert-management/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/projects/cert-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/projects/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute + '/projects/kms/$projectId/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/projects/secret-management/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/projects/secret-management/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/projects/secret-scanning/$projectId/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/projects/secret-scanning/$projectId/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/projects/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/projects/ssh/$projectId/audit-logs': typeof projectAuditLogsPageRouteSshRoute '/projects/cert-management/$projectId/certificate-templates': typeof certManagerPkiTemplateListPageRouteRoute '/projects/cert-management/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute '/projects/secret-management/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute @@ -5075,14 +5173,19 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs': typeof projectAuditLogsPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertManagerLayoutSubscribersRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute + '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs': typeof projectAuditLogsPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute + '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs': typeof projectAuditLogsPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretManagementProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management': typeof projectAccessControlPageRouteSecretScanningRoute + '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs': typeof projectAuditLogsPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute + '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs': typeof projectAuditLogsPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/': typeof certManagerPkiTemplateListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute @@ -5295,14 +5398,19 @@ export interface FileRouteTypes { | '/projects/ssh/$projectId/overview' | '/projects/ssh/$projectId/settings' | '/projects/cert-management/$projectId/access-management' + | '/projects/cert-management/$projectId/audit-logs' | '/projects/cert-management/$projectId/certificate-templates' | '/projects/cert-management/$projectId/subscribers' | '/projects/kms/$projectId/access-management' + | '/projects/kms/$projectId/audit-logs' | '/projects/secret-management/$projectId/access-management' + | '/projects/secret-management/$projectId/audit-logs' | '/projects/secret-management/$projectId/integrations' | '/projects/secret-scanning/$projectId/access-management' + | '/projects/secret-scanning/$projectId/audit-logs' | '/projects/secret-scanning/$projectId/data-sources' | '/projects/ssh/$projectId/access-management' + | '/projects/ssh/$projectId/audit-logs' | '/projects/cert-management/$projectId/certificate-templates/' | '/projects/cert-management/$projectId/subscribers/' | '/projects/secret-management/$projectId/integrations/' @@ -5504,10 +5612,15 @@ export interface FileRouteTypes { | '/projects/ssh/$projectId/overview' | '/projects/ssh/$projectId/settings' | '/projects/cert-management/$projectId/access-management' + | '/projects/cert-management/$projectId/audit-logs' | '/projects/kms/$projectId/access-management' + | '/projects/kms/$projectId/audit-logs' | '/projects/secret-management/$projectId/access-management' + | '/projects/secret-management/$projectId/audit-logs' | '/projects/secret-scanning/$projectId/access-management' + | '/projects/secret-scanning/$projectId/audit-logs' | '/projects/ssh/$projectId/access-management' + | '/projects/ssh/$projectId/audit-logs' | '/projects/cert-management/$projectId/certificate-templates' | '/projects/cert-management/$projectId/subscribers' | '/projects/secret-management/$projectId/integrations' @@ -5725,14 +5838,19 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers' | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources' | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/' | '/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/' @@ -6331,6 +6449,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers", "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName", @@ -6349,6 +6468,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId", @@ -6365,6 +6485,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secret-rotation", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug", "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId", @@ -6381,6 +6502,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId", @@ -6397,6 +6519,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/overview", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/settings", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId", "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/groups/$groupId", @@ -6485,6 +6608,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-cert-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout", @@ -6504,10 +6631,18 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-kms.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-kms.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/access-management": { "filePath": "project/AccessControlPage/route-secret-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-secret-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout", @@ -6596,6 +6731,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-secret-scanning.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-secret-scanning.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout", @@ -6608,6 +6747,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-ssh.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout" }, + "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/audit-logs": { + "filePath": "project/AuditLogsPage/route-ssh.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout" + }, "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/": { "filePath": "cert-manager/PkiTemplateListPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 353aa7a5d..2e63be383 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -63,6 +63,7 @@ const secretManagerRoutes = route("/projects/secret-management/$projectId", [ ) ]) ]), + route("/audit-logs", "project/AuditLogsPage/route-secret-manager.tsx"), route("/access-management", "project/AccessControlPage/route-secret-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-manager.tsx"), @@ -312,6 +313,7 @@ const certManagerRoutes = route("/projects/cert-management/$projectId", [ route("/ca/$caName", "cert-manager/CertAuthDetailsByIDPage/route.tsx"), route("/pki-collections/$collectionId", "cert-manager/PkiCollectionDetailsByIDPage/routes.tsx"), route("/settings", "cert-manager/SettingsPage/route.tsx"), + route("/audit-logs", "project/AuditLogsPage/route-cert-manager.tsx"), route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-cert-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-cert-manager.tsx"), @@ -325,6 +327,7 @@ const kmsRoutes = route("/projects/kms/$projectId", [ route("/overview", "kms/OverviewPage/route.tsx"), route("/kmip", "kms/KmipPage/route.tsx"), route("/settings", "kms/SettingsPage/route.tsx"), + route("/audit-logs", "project/AuditLogsPage/route-kms.tsx"), route("/access-management", "project/AccessControlPage/route-kms.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-kms.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-kms.tsx"), @@ -341,6 +344,7 @@ const sshRoutes = route("/projects/ssh/$projectId", [ route("/ca/$caId", "ssh/SshCaByIDPage/route.tsx"), route("/ssh-host-groups/$sshHostGroupId", "ssh/SshHostGroupDetailsByIDPage/route.tsx"), route("/settings", "ssh/SettingsPage/route.tsx"), + route("/audit-logs", "project/AuditLogsPage/route-ssh.tsx"), route("/access-management", "project/AccessControlPage/route-ssh.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-ssh.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-ssh.tsx"), @@ -357,6 +361,7 @@ const secretScanningRoutes = route("/projects/secret-scanning/$projectId", [ ]), route("/findings", "secret-scanning/SecretScanningFindingsPage/route.tsx"), route("/settings", "secret-scanning/SettingsPage/route.tsx"), + route("/audit-logs", "project/AuditLogsPage/route-secret-scanning.tsx"), route("/access-management", "project/AccessControlPage/route-secret-scanning.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-scanning.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-scanning.tsx"), From dd0c07fb95efd14ec27ff75271b470068cb98331 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 23 Jul 2025 18:18:59 -0700 Subject: [PATCH 12/79] improvements: remove fixed css --- frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 88f71f8e0..f62b3cb8c 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -1445,7 +1445,6 @@ export const OverviewPage = () => { } icon={faFolderBlank} iconSize="3x" - className="fixed" >
); } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index c11ff454d..3c2c0c859 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -948,12 +948,12 @@ const Page = () => { /> )} {noAccessSecretCount > 0 && } - {!canReadSecret && - !canReadDynamicSecret && - !canReadSecretImports && - folders?.length === 0 && }
+ {!canReadSecret && + !canReadDynamicSecret && + !canReadSecretImports && + folders?.length === 0 && } {!isDetailsLoading && (totalCount > 0 || pendingChanges.secrets.length > 0 || diff --git a/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/ProjectScanningConfigTab.tsx b/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/ProjectScanningConfigTab.tsx index 73fd26c8b..e8d3206d8 100644 --- a/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/ProjectScanningConfigTab.tsx +++ b/frontend/src/pages/secret-scanning/SettingsPage/components/ProjectScanningConfigTab/ProjectScanningConfigTab.tsx @@ -1,6 +1,6 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; -import { ContentLoader, EmptyState } from "@app/components/v2"; +import { AccessRestrictedBanner, ContentLoader, EmptyState } from "@app/components/v2"; import { useSubscription, useWorkspace } from "@app/context"; import { useGetSecretScanningConfig } from "@app/hooks/api/secretScanningV2"; @@ -16,16 +16,14 @@ export const ProjectScanningConfigTab = () => { if (!subscription.secretScanning) { return ( -
- +
+ Your current plan doesn't support Secret Scanning.
Please contact Infisical Support or reach out through our Slack channel for assistance. - + } />
From 4e960445a4abcde9bd0d97ad14ead5430ee7cf13 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 24 Jul 2025 15:56:14 -0700 Subject: [PATCH 30/79] chore: remove unused tw css --- .../v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx b/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx index 27fa3c982..1979039dd 100644 --- a/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx +++ b/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx @@ -16,7 +16,7 @@ export const AccessRestrictedBanner = ({ ) }: Props) => { return ( -
+
{title}
{body}
From ad905b2ff73b02def307f9a1446dbf9c9d525cd1 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Thu, 24 Jul 2025 20:42:39 -0300 Subject: [PATCH 31/79] Fix secret reminders migration job --- .../src/services/reminder/reminder-queue.ts | 4 +- .../src/services/reminder/reminder-service.ts | 5 +-- .../secret-v2-bridge/secret-v2-bridge-dal.ts | 45 ++++++++++++++++++- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/backend/src/services/reminder/reminder-queue.ts b/backend/src/services/reminder/reminder-queue.ts index 4e31c8a6d..1487a63f3 100644 --- a/backend/src/services/reminder/reminder-queue.ts +++ b/backend/src/services/reminder/reminder-queue.ts @@ -11,7 +11,7 @@ import { TReminderServiceFactory } from "./reminder-types"; type TDailyReminderQueueServiceFactoryDep = { reminderService: TReminderServiceFactory; queueService: TQueueServiceFactory; - secretDAL: Pick; + secretDAL: Pick; secretReminderRecipientsDAL: Pick; }; @@ -69,7 +69,7 @@ export const dailyReminderQueueServiceFactory = ({ // Find existing secrets with pagination // eslint-disable-next-line no-await-in-loop - const secrets = await secretDAL.findSecretsWithReminderRecipients(batchIds, REMINDER_PRUNE_BATCH_SIZE); + const secrets = await secretDAL.findSecretsWithReminderRecipientsOld(batchIds, REMINDER_PRUNE_BATCH_SIZE); const secretsWithReminder = secrets.filter((secret) => secret.reminderRepeatDays); const foundSecretIds = new Set(secretsWithReminder.map((secret) => secret.id)); diff --git a/backend/src/services/reminder/reminder-service.ts b/backend/src/services/reminder/reminder-service.ts index ddccbbf62..a03e9cddd 100644 --- a/backend/src/services/reminder/reminder-service.ts +++ b/backend/src/services/reminder/reminder-service.ts @@ -308,12 +308,11 @@ export const reminderServiceFactory = ({ ); const newReminders = await reminderDAL.insertMany( - processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, projectId }) => ({ + processedReminders.map(({ secretId, message, repeatDays, nextReminderDate }) => ({ secretId, message, repeatDays, - nextReminderDate, - projectId + nextReminderDate })), tx ); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 4cbd1d783..c2a72f2e6 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -875,6 +875,48 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { } }; + const findSecretsWithReminderRecipientsOld = async (ids: string[], limit: number, tx?: Knex) => { + try { + // Create a subquery to get limited secret IDs + const limitedSecretIds = (tx || db)(TableName.SecretV2) + .whereIn(`${TableName.SecretV2}.id`, ids) + .limit(limit) + .select("id"); + + // Join with all recipients for the limited secrets + const docs = await (tx || db)(TableName.SecretV2) + .whereIn(`${TableName.SecretV2}.id`, limitedSecretIds) + .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`) + .leftJoin( + TableName.SecretReminderRecipients, + `${TableName.SecretV2}.id`, + `${TableName.SecretReminderRecipients}.secretId` + ) + .select(selectAllTableCols(TableName.SecretV2)) + .select(db.ref("userId").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientUserId")); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + _id: el.id, + ...SecretsV2Schema.parse(el) + }), + childrenMapper: [ + { + key: "reminderRecipientUserId", + label: "recipients" as const, + mapper: ({ reminderRecipientUserId }) => reminderRecipientUserId + } + ] + }); + + return data; + } catch (error) { + throw new DatabaseError({ error, name: "findSecretsWithReminderRecipientsOld" }); + } + }; + return { ...secretOrm, update, @@ -893,6 +935,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { findOne, find, invalidateSecretCacheByProjectId, - findSecretsWithReminderRecipients + findSecretsWithReminderRecipients, + findSecretsWithReminderRecipientsOld }; }; From 7917a767e6832999d9a90ef09b6893b096568940 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 25 Jul 2025 04:57:15 +0400 Subject: [PATCH 32/79] Update docs.json --- docs/docs.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index e0d7a2031..ebbc31500 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -206,13 +206,6 @@ "documentation/platform/external-migrations/vault" ] }, - { - "group": "External Migrations", - "pages": [ - "documentation/platform/workflow-integrations/slack-integration", - "documentation/platform/workflow-integrations/microsoft-teams-integration" - ] - }, { "group": "Admin Consoles", "pages": [ From 5df7539f6526c981aefa88bf71c1320f6f6848ec Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 24 Jul 2025 21:43:18 -0400 Subject: [PATCH 33/79] Swap away from using octokit due to gateway compatibility issues --- .../github/github-connection-fns.ts | 381 ++++++++---------- .../secret-sync/github/github-sync-fns.ts | 258 ++++++------ 2 files changed, 296 insertions(+), 343 deletions(-) diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index 2607d8e9e..5f4057bd2 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -1,7 +1,7 @@ import { createAppAuth } from "@octokit/auth-app"; -import { Octokit } from "@octokit/rest"; import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; import https from "https"; +import RE2 from "re2"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; @@ -29,251 +29,196 @@ export const getGitHubConnectionListItem = () => { }; }; -export const getGitHubClient = ( - appConnection: TGitHubConnection, - octokitOptions: Partial<{ baseUrl: string; request: { agent?: https.Agent } }> -) => { - const appCfg = getConfig(); - - const { method, credentials } = appConnection; - const { baseUrl, request } = octokitOptions; - - let client: Octokit; - - const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID; - const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY; - - switch (method) { - case GitHubConnectionMethod.App: - if (!appId || !appPrivateKey) { - throw new InternalServerError({ - message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured` - }); - } - - client = new Octokit({ - authStrategy: createAppAuth, - auth: { - appId, - privateKey: appPrivateKey, - installationId: credentials.installationId - }, - baseUrl, - request - }); - break; - case GitHubConnectionMethod.OAuth: - client = new Octokit({ - auth: credentials.accessToken, - baseUrl, - request - }); - break; - default: - throw new InternalServerError({ - message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}` - }); - } - - return client; -}; - -export const executeWithGitHubGateway = async ( - appConnection: TGitHubConnection, - gatewayService: Pick, - operation: (client: Octokit) => Promise -): Promise => { - const { - gatewayId, - credentials: { host: hostParam } - } = appConnection; - - const host = hostParam || "api.github.com"; - - if (gatewayId && gatewayService) { - const [targetHost] = await verifyHostInputValidity(host, true); - const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); - const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); - - return withGatewayProxy( - async (proxyPort) => { - const agent = new https.Agent({ - servername: targetHost, - rejectUnauthorized: true - }); - - const client = getGitHubClient(appConnection, { - baseUrl: `https://localhost:${proxyPort}`, - request: { agent } - }); - - return operation(client); - }, - { - protocol: GatewayProxyProtocol.Tcp, - targetHost, - targetPort: 443, - relayHost, - relayPort: Number(relayPort), - identityId: relayDetails.identityId, - orgId: relayDetails.orgId, - tlsOptions: { - ca: relayDetails.certChain, - cert: relayDetails.certificate, - key: relayDetails.privateKey.toString() - } - } - ); - } - - // Non-gateway path - const client = getGitHubClient(appConnection, { - baseUrl: `https://${host}` - }); - - return operation(client); -}; - -// For non-octokit requests export const requestWithGitHubGateway = async ( - appConnection: TGitHubConnectionConfig, + appConnection: { gatewayId?: string | null; credentials: { host?: string } }, gatewayService: Pick, requestConfig: AxiosRequestConfig ): Promise> => { - const { - gatewayId, - credentials: { host: hostParam } - } = appConnection; + const { gatewayId } = appConnection; + + // If gateway isn't set up, don't proxy request + if (!gatewayId) { + return httpRequest.request(requestConfig); + } const url = new URL(requestConfig.url as string); - const host = hostParam || url.host || "github.com"; - if (gatewayId && gatewayService) { - const [targetHost] = await verifyHostInputValidity(host, true); - const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); - const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); + const [targetHost] = await verifyHostInputValidity(url.host, true); + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); + const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); - return withGatewayProxy( - async (proxyPort) => { - const proxyAgent = new https.Agent({ - servername: targetHost, - rejectUnauthorized: true - }); + return withGatewayProxy( + async (proxyPort) => { + const httpsAgent = new https.Agent({ + servername: targetHost + }); - url.protocol = "https:"; - url.host = `localhost:${proxyPort}`; + url.protocol = "https:"; + url.host = `localhost:${proxyPort}`; - const finalRequestConfig: AxiosRequestConfig = { - ...requestConfig, - url: url.toString(), - httpsAgent: proxyAgent, - headers: { - ...requestConfig.headers, - Host: targetHost - } - }; - - try { - return await httpRequest.request(finalRequestConfig); - } catch (error) { - const axiosError = error as AxiosError; - logger.error("Error during GitHub gateway request:", axiosError.message, axiosError.response?.data); - throw error; - } - }, - { - protocol: GatewayProxyProtocol.Tcp, - targetHost, - targetPort: 443, - relayHost, - relayPort: Number(relayPort), - identityId: relayDetails.identityId, - orgId: relayDetails.orgId, - tlsOptions: { - ca: relayDetails.certChain, - cert: relayDetails.certificate, - key: relayDetails.privateKey.toString() + const finalRequestConfig: AxiosRequestConfig = { + ...requestConfig, + url: url.toString(), + httpsAgent, + headers: { + ...requestConfig.headers, + Host: targetHost } + }; + + try { + return await httpRequest.request(finalRequestConfig); + } catch (error) { + const axiosError = error as AxiosError; + logger.error("Error during GitHub gateway request:", axiosError.message, axiosError.response?.data); + throw error; } - ); + }, + { + protocol: GatewayProxyProtocol.Tcp, + targetHost, + targetPort: 443, + relayHost, + relayPort: Number(relayPort), + identityId: relayDetails.identityId, + orgId: relayDetails.orgId, + tlsOptions: { + ca: relayDetails.certChain, + cert: relayDetails.certificate, + key: relayDetails.privateKey.toString() + } + } + ); +}; + +export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) => { + const appCfg = getConfig(); + const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID; + const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY; + + if (!appId || !appPrivateKey) { + throw new InternalServerError({ + message: `GitHub App keys are not configured.` + }); } - if (!url.host) { - url.protocol = "https:"; - url.host = host; + if (appConnection.method !== GitHubConnectionMethod.App) { + throw new InternalServerError({ message: "Cannot generate GitHub App token for non-app connection" }); } - const finalRequestConfig: AxiosRequestConfig = { - ...requestConfig, - url: url.toString() - }; + const appAuth = createAppAuth({ + appId, + privateKey: appPrivateKey, + installationId: appConnection.credentials.installationId + }); - return httpRequest.request(finalRequestConfig); + const { token } = await appAuth({ type: "installation" }); + return token; +}; + +export const makePaginatedGitHubRequest = async ( + appConnection: TGitHubConnection, + gatewayService: Pick, + path: string, + dataMapper?: (data: R) => T[] +): Promise => { + const { credentials, method } = appConnection; + + const token = + method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection); + let url: string | null = `https://api.${credentials.host || "github.com"}${path}`; + let results: T[] = []; + + while (url) { + // eslint-disable-next-line no-await-in-loop + const response: AxiosResponse = await requestWithGitHubGateway(appConnection, gatewayService, { + url, + method: "GET", + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28" + } + }); + + const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]); + results = results.concat(items); + + const linkHeader = response.headers.link as string | undefined; + const nextLink = + typeof linkHeader === "string" ? linkHeader.split(",").find((s) => s.includes('rel="next"')) : undefined; + if (nextLink) { + url = new RE2(/<(.+)>/).exec(nextLink)?.[1] || null; + } else { + url = null; + } + } + + return results; }; type GitHubOrganization = { login: string; id: number; + type: string; }; type GitHubRepository = { id: number; name: string; owner: GitHubOrganization; + permissions?: { + admin: boolean; + maintain: boolean; + push: boolean; + triage: boolean; + pull: boolean; + }; +}; + +type GitHubEnvironment = { + id: number; + name: string; }; export const getGitHubRepositories = async ( appConnection: TGitHubConnection, gatewayService: Pick ) => { - return executeWithGitHubGateway(appConnection, gatewayService, async (client) => { - let repositories: GitHubRepository[]; + if (appConnection.method === GitHubConnectionMethod.App) { + return makePaginatedGitHubRequest( + appConnection, + gatewayService, + "/installation/repositories", + (data) => data.repositories + ); + } - switch (appConnection.method) { - case GitHubConnectionMethod.App: - repositories = await client.paginate("GET /installation/repositories"); - break; - case GitHubConnectionMethod.OAuth: - default: - repositories = (await client.paginate("GET /user/repos")).filter((repo) => repo.permissions?.admin); - break; - } - - return repositories; - }); + const repos = await makePaginatedGitHubRequest(appConnection, gatewayService, "/user/repos"); + return repos.filter((repo) => repo.permissions?.admin); }; export const getGitHubOrganizations = async ( appConnection: TGitHubConnection, gatewayService: Pick ) => { - return executeWithGitHubGateway(appConnection, gatewayService, async (client) => { - let organizations: GitHubOrganization[]; + if (appConnection.method === GitHubConnectionMethod.App) { + const installationRepositories = await makePaginatedGitHubRequest< + GitHubRepository, + { repositories: GitHubRepository[] } + >(appConnection, gatewayService, "/installation/repositories", (data) => data.repositories); - switch (appConnection.method) { - case GitHubConnectionMethod.App: { - const installationRepositories = await client.paginate("GET /installation/repositories"); - - const organizationMap: Record = {}; - - installationRepositories.forEach((repo) => { - if (repo.owner.type === "Organization") { - organizationMap[repo.owner.id] = repo.owner; - } - }); - - organizations = Object.values(organizationMap); - - break; + const organizationMap: Record = {}; + installationRepositories.forEach((repo) => { + if (repo.owner.type === "Organization") { + organizationMap[repo.owner.id] = repo.owner; } - case GitHubConnectionMethod.OAuth: - default: - organizations = await client.paginate("GET /user/orgs"); - break; - } + }); - return organizations; - }); + return Object.values(organizationMap); + } + + return makePaginatedGitHubRequest(appConnection, gatewayService, "/user/orgs"); }; export const getGitHubEnvironments = async ( @@ -282,23 +227,18 @@ export const getGitHubEnvironments = async ( owner: string, repo: string ) => { - return executeWithGitHubGateway(appConnection, gatewayService, async (client) => { - try { - const environments = await client.paginate("GET /repos/{owner}/{repo}/environments", { - owner, - repo - }); - - return environments; - } catch (e) { - // repo doesn't have envs - if ((e as { status: number }).status === 404) { - return []; - } - - throw e; - } - }); + try { + return await makePaginatedGitHubRequest( + appConnection, + gatewayService, + `/repos/${owner}/${repo}/environments`, + (data) => data.environments + ); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response?.status === 404) return []; + throw error; + } }; export type GithubTokenRespData = { @@ -352,7 +292,6 @@ export const validateGitHubConnectionCredentials = async ( let tokenResp: AxiosResponse; const host = credentials.host || "github.com"; - const apiHost = credentials.host ? `api.${credentials.host}` : "api.github.com"; try { tokenResp = await requestWithGitHubGateway(config, gatewayService, { @@ -406,7 +345,7 @@ export const validateGitHubConnectionCredentials = async ( }; }[]; }>(config, gatewayService, { - url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", apiHost), + url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", `api.${host}`), headers: { Accept: "application/json", Authorization: `Bearer ${tokenResp.data.access_token}`, diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index 59affa2b7..580d77bca 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -1,8 +1,12 @@ -import { Octokit } from "@octokit/rest"; import sodium from "libsodium-wrappers"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; -import { executeWithGitHubGateway } from "@app/services/app-connection/github"; +import { + getGitHubAppAuthToken, + GitHubConnectionMethod, + makePaginatedGitHubRequest, + requestWithGitHubGateway +} from "@app/services/app-connection/github"; import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; @@ -13,151 +17,155 @@ import { TGitHubPublicKey, TGitHubSecret, TGitHubSecretPayload, TGitHubSyncWithC // TODO: rate limit handling -const getEncryptedSecrets = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => { - let encryptedSecrets: TGitHubSecret[]; - - const { destinationConfig } = secretSync; +const getEncryptedSecrets = async ( + secretSync: TGitHubSyncWithCredentials, + gatewayService: Pick +) => { + const { destinationConfig, connection } = secretSync; + let path: string; switch (destinationConfig.scope) { case GitHubSyncScope.Organization: { - encryptedSecrets = await client.paginate("GET /orgs/{org}/actions/secrets", { - org: destinationConfig.org - }); + path = `/orgs/${destinationConfig.org}/actions/secrets`; break; } case GitHubSyncScope.Repository: { - encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/actions/secrets", { - owner: destinationConfig.owner, - repo: destinationConfig.repo - }); - + path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets`; break; } case GitHubSyncScope.RepositoryEnvironment: default: { - encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets", { - owner: destinationConfig.owner, - repo: destinationConfig.repo, - environment_name: destinationConfig.env - }); + path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets`; break; } } - return encryptedSecrets; + return makePaginatedGitHubRequest( + connection, + gatewayService, + path, + (data) => data.secrets + ); }; -const getPublicKey = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => { - let publicKey: TGitHubPublicKey; - - const { destinationConfig } = secretSync; +const getPublicKey = async ( + secretSync: TGitHubSyncWithCredentials, + gatewayService: Pick, + token: string +) => { + const { destinationConfig, connection } = secretSync; + let path: string; switch (destinationConfig.scope) { case GitHubSyncScope.Organization: { - publicKey = ( - await client.request("GET /orgs/{org}/actions/secrets/public-key", { - org: destinationConfig.org - }) - ).data; + path = `/orgs/${destinationConfig.org}/actions/secrets/public-key`; break; } case GitHubSyncScope.Repository: { - publicKey = ( - await client.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", { - owner: destinationConfig.owner, - repo: destinationConfig.repo - }) - ).data; + path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/public-key`; break; } case GitHubSyncScope.RepositoryEnvironment: default: { - publicKey = ( - await client.request("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key", { - owner: destinationConfig.owner, - repo: destinationConfig.repo, - environment_name: destinationConfig.env - }) - ).data; + path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/public-key`; break; } } - return publicKey; + const response = await requestWithGitHubGateway(connection, gatewayService, { + url: `https://api.${connection.credentials.host || "github.com"}${path}`, + method: "GET", + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28" + } + }); + + return response.data; }; const deleteSecret = async ( - client: Octokit, secretSync: TGitHubSyncWithCredentials, + gatewayService: Pick, + token: string, encryptedSecret: TGitHubSecret ) => { - const { destinationConfig } = secretSync; + const { destinationConfig, connection } = secretSync; + let path: string; switch (destinationConfig.scope) { case GitHubSyncScope.Organization: { - await client.request(`DELETE /orgs/{org}/actions/secrets/{secret_name}`, { - org: destinationConfig.org, - secret_name: encryptedSecret.name - }); + path = `/orgs/${destinationConfig.org}/actions/secrets/${encryptedSecret.name}`; break; } case GitHubSyncScope.Repository: { - await client.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { - owner: destinationConfig.owner, - repo: destinationConfig.repo, - secret_name: encryptedSecret.name - }); + path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/${encryptedSecret.name}`; break; } case GitHubSyncScope.RepositoryEnvironment: default: { - await client.request("DELETE /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", { - owner: destinationConfig.owner, - repo: destinationConfig.repo, - environment_name: destinationConfig.env, - secret_name: encryptedSecret.name - }); + path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/${encryptedSecret.name}`; break; } } + + await requestWithGitHubGateway(connection, gatewayService, { + url: `https://api.${connection.credentials.host || "github.com"}${path}`, + method: "DELETE", + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28" + } + }); }; -const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials, payload: TGitHubSecretPayload) => { - const { destinationConfig } = secretSync; +const putSecret = async ( + secretSync: TGitHubSyncWithCredentials, + gatewayService: Pick, + token: string, + payload: TGitHubSecretPayload +) => { + const { destinationConfig, connection } = secretSync; + + let path: string; + let body: Record = payload; switch (destinationConfig.scope) { case GitHubSyncScope.Organization: { const { visibility, selectedRepositoryIds } = destinationConfig; - - await client.request(`PUT /orgs/{org}/actions/secrets/{secret_name}`, { - org: destinationConfig.org, + path = `/orgs/${destinationConfig.org}/actions/secrets/${payload.secret_name}`; + body = { ...payload, visibility, ...(visibility === GitHubSyncVisibility.Selected && { selected_repository_ids: selectedRepositoryIds }) - }); + }; break; } case GitHubSyncScope.Repository: { - await client.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { - owner: destinationConfig.owner, - repo: destinationConfig.repo, - ...payload - }); + path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/${payload.secret_name}`; break; } case GitHubSyncScope.RepositoryEnvironment: default: { - await client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", { - owner: destinationConfig.owner, - repo: destinationConfig.repo, - environment_name: destinationConfig.env, - ...payload - }); + path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/${payload.secret_name}`; break; } } + + await requestWithGitHubGateway(connection, gatewayService, { + url: `https://api.${connection.credentials.host || "github.com"}${path}`, + method: "PUT", + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28" + }, + data: body + }); }; export const GithubSyncFns = { @@ -192,50 +200,52 @@ export const GithubSyncFns = { ); } - await executeWithGitHubGateway(secretSync.connection, gatewayService, async (client) => { - const encryptedSecrets = await getEncryptedSecrets(client, secretSync); + const { connection } = secretSync; + const token = + connection.method === GitHubConnectionMethod.OAuth + ? connection.credentials.accessToken + : await getGitHubAppAuthToken(connection); - const publicKey = await getPublicKey(client, secretSync); + const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService); + const publicKey = await getPublicKey(secretSync, gatewayService, token); - await sodium.ready.then(async () => { - for await (const key of Object.keys(secretMap)) { - // convert secret & base64 key to Uint8Array. - const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL); - const binarySecretValue = sodium.from_string(secretMap[key].value); + await sodium.ready; + for await (const key of Object.keys(secretMap)) { + // convert secret & base64 key to Uint8Array. + const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL); + const binarySecretValue = sodium.from_string(secretMap[key].value); - // encrypt secret using libsodium - const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey); + // encrypt secret using libsodium + const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey); - // convert encrypted Uint8Array to base64 - const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL); + // convert encrypted Uint8Array to base64 + const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL); - try { - await putSecret(client, secretSync, { - secret_name: key, - encrypted_value: encryptedSecretValue, - key_id: publicKey.key_id - }); - } catch (error) { - throw new SecretSyncError({ - error, - secretKey: key - }); - } - } - }); - - if (secretSync.syncOptions.disableSecretDeletion) return; - - for await (const encryptedSecret of encryptedSecrets) { - if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) - // eslint-disable-next-line no-continue - continue; - - if (!(encryptedSecret.name in secretMap)) { - await deleteSecret(client, secretSync, encryptedSecret); - } + try { + await putSecret(secretSync, gatewayService, token, { + secret_name: key, + encrypted_value: encryptedSecretValue, + key_id: publicKey.key_id + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); } - }); + } + + if (secretSync.syncOptions.disableSecretDeletion) return; + + for await (const encryptedSecret of encryptedSecrets) { + if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) + // eslint-disable-next-line no-continue + continue; + + if (!(encryptedSecret.name in secretMap)) { + await deleteSecret(secretSync, gatewayService, token, encryptedSecret); + } + } }, getSecrets: async (secretSync: TGitHubSyncWithCredentials) => { throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); @@ -245,14 +255,18 @@ export const GithubSyncFns = { secretMap: TSecretMap, gatewayService: Pick ) => { - await executeWithGitHubGateway(secretSync.connection, gatewayService, async (client) => { - const encryptedSecrets = await getEncryptedSecrets(client, secretSync); + const { connection } = secretSync; + const token = + connection.method === GitHubConnectionMethod.OAuth + ? connection.credentials.accessToken + : await getGitHubAppAuthToken(connection); - for await (const encryptedSecret of encryptedSecrets) { - if (encryptedSecret.name in secretMap) { - await deleteSecret(client, secretSync, encryptedSecret); - } + const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService); + + for await (const encryptedSecret of encryptedSecrets) { + if (encryptedSecret.name in secretMap) { + await deleteSecret(secretSync, gatewayService, token, encryptedSecret); } - }); + } } }; From 4b6f9fdec2f228f1a47001e732e43955885a7fb0 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 24 Jul 2025 21:47:38 -0400 Subject: [PATCH 34/79] docs --- .../github/create-github-app-method.png | Bin 600585 -> 0 bytes docs/integrations/app-connections/github.mdx | 5 +++++ 2 files changed, 5 insertions(+) diff --git a/docs/images/app-connections/github/create-github-app-method.png b/docs/images/app-connections/github/create-github-app-method.png index 640fb0213a474d37d3658956c30ba43858052862..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 600585 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ΝRSjCyeC@ zZJ5XRcH)i=cBL7A0w#-b17qQoZe=j3OxB9p(*KXiLC>qM{!#5|vY`It@6{eq2=d?4 zNvBFa^uiunTgZaeu=<`5rv=y5kZXQad(Qnq{KiIXwq1JdP__^IN!tgHXEpoFL=u74 z=i@&v-rW%;Pe0_;>tqw2flo_Wb1+WJZwen}<Xin#772tA z;GN=Lhd0z;Bu6G&_2>1&x8!6c&1;gDa|B`Gdq45du67q$&Y6?znkzE2xIq;#T{Aan zIsS%}=wfE(6)fmX>_n%}b;QwuDL<3bGcz!jbOW8_F4u+O@pS@brn#lXxq^?Mm$$0A z+FcAr`idULyS$}ho(@ea=22Hl{r>~>fB`n8N#8I%)(;C7m+C;(A}s|>Ng)9L>jHg4 z=IHFKg>xo(vbXn*uAbh*BMl|=JsDOTTi?H@!+dt4F%`CPofWd|_>MR+Fm{PFUY;mx zXe-uvX+o`(I+G4-y`}V-wdNWaEwI~-8;cdW0w$y6-o+;K1hUI=|06T-@aF&?Juhd9 zQdO+70R|vtafcsNfhUuim77+$5AV4sZPsU<4UVM4=Bl%g*iujNZ<`;_U*3*Dp62DjzA-?_zk|C;4^* zX4V4i*z?uEKLG#}xNiTy04A~R=7DMAEE5LecGJ(t9L|Vj1!KsNuV24@(gdd5jFy_0 zR%`0s9s+Y=-Z)h=tYxLHqtV)xB_(=Qxx*iy1&x9}4lFpk-WP_CZ>-L#Ro3L}bIelt z&?6_z%t191RBT4sKeACX{FWMU zb$$$p1mtj?SIz1MV6@*)znr)Hdi?CqAp(EYto}KZ|7n7!rR*PgtRQJgGbD#Eo1I<* zZSgXl&alg?XOMhw15@~QP;j;6yl)2>2;wOr)OT&nl5+r8dg=%&k0wu0Vm>f1Xmq{= zn}-S-7S37rX<66=65`TNLk-p3@qaVaq~+j_u%zOLLSmB({s{*geSGjDj_Z{-wT`ay z667t>d2ux=25K&;M&AI6=pM0xEfA>ceYoHvh|Ad$7NtowL7rM~ywQjC`>l^YlO?~E zcW|)mA)c%_U4FD#o=>$@b*#(6xM}igS11^qr|*t?eDihAGpncb@V6}+Z3b!e>ZxnR zGL!!x;Cvov;Rz>jzW{f+ZVs8a31%*V32Vx+?wp4H-(l8@D%57oPy4!nq3SzER!p{Sg@y8!4cQs~*ZGJ;$dVE~m;bE|E2=IAG^Y{iv62YK96SJYV;9B4W?K-rrDQGoXwr#0b24W z=NP}mGyKKr`6&u5n{T&Q*(U3m>|@TP3Y1^{Q1*%c_e2)ugqw3pi8p%$-w}$+fK~B_ zgyZ)j66nmumZE2_dddfN*!-ZDZKjidmTlz4#H(AtysB{xO-(GBbfRzl8mCgiHAj2r z8fnil4>^Lu!umlOP{~oukHXhDpX!iyv#eL;{PZa`?ke%6nwlrD&+=DHG@&TlxiRw| z370{heYqC@C@eOL+Kmn;Mtj=_kmYjI2u?7#)tQ_{bp1u7~iw_5pO*FnJ zJ^$m3`aHYDVu?GLna0!AtPDD^WEEpS6k0FCeEEJhe2a?C%gM_(ei$D&+GyoY+T@C} zUL33QLj2B378fV7H& zNyE_Hor8dMnKX=m2ud@=kOPv^NaxVqFf-J^|FFM({I8%K2aT7yW*((^Zy+dkL|!X@?5n;W?dpQa1WE(mi7*b7%W7of0(?p^(8 zE5eXLx80LAA@z8YGVo`wRt1bB4|72bYlS}WtWE&?-vb}IhyHEl93xxei{*T?`jOnb zYd(ur{d|A6cetA?e6{}!_tj>N`@~dHF;BK$CL%AR4v_IX>@%qMyZdxB-Y%btiVAEF z$0?RkS@t}w0XrXv?8PslN1Hy<2k1LGyETdKA$!o43g_|$piQSe*!+ZVXQojdD|h~M zQnYqcDBe9_XC_b&v$g8)?(8u5%kAX_*7Af;BCWXLr^-~o5Usx?wm0Ccb91z$sSb*h zx*532<4k$c^Yi_qU4GFE+p=_xjk9Bc%?tm#!Z3QOMx)S7)TX&w$-c({!>7EI5$yPn znd%U65@Vtau$YAq2fHJ%{Fo2^Uy=|@Gbf||7kX?UjZLWqY!7|u=H`&_*vNh4|3WM8 zklS$l^tAS5JK&r#U*V+h{RA=~Km(Q34jAds=8eP--&p7kF$!PqNDnDl*1f}SsnDM8 zT)&t^_w#|sEg^rfq&DSAj|F>WoSt#p*1T0!3;=}%ad7^uBTwm>zWf#<=>2dD1${P^&Fsbq-SBZ^~#mRJ@UhE^7g z`CDM|c@g!tT4NX;rmt-dOL%NfuByXiH!JKXmVnTt|Lj<%@9tc-@7_X*@9BcI-nM{h zz`lmuk%+$ZJaPc60asRIUPKXLPTSwXV#>i;>eh_!)t$tX)`=IE=o3BiU zLJ`UW3&2<-u%N`03geeOe}MB_?4X7WV()j?_^&v{31-FSLE%NCGQ2$f=ogb!wqYr< zGE=jd(Ha?VI|q4g@vZUli8LwqKhF+99l{u6WPwWAlmJ`Ub(59{?H`)9c4im64;K?4 zsWMdH*6;uunjw<4yP@#2Bk0Zyv@pux9H{Ggom{s>rO2=XVNrL@1)#r{E|Bgm>wnw~ zmSIr6jYgS-CmR=fiqg1!6T&YEd zBZd0x;m7CJ=VzB!j|(YV;%{k8ukKX0gfe6-+fH-{ftOP7v9*pC3s zM?*1`@0m`&Yq4uvk0%1}Y44YB|J=Lqsf4I+6C2RWd#tMGb*O~%Y{t$fTnn3DBCUncgPPNW0upLg0emdLQUHOVr!{cVa7`eTsACTv`Q3&+(=j)I6 zxUsB|_hza>supAr0g}61r}7**Z;Y>d7c?Piv$n9{gy` z*tXmt;}0W!xC29>RX}Go^7Jeu1s3PQ{0dc`$vW(@RcZjAyFJoPTZdbgi_mk~v>6TL z);rA>wNA4L*qu|x@=qx9zIvPO3q9R!18P%MI_&AxZ^w*LCm|%XlN55NE1p47BnzbX zBsYBFLHDKexBtA{n;dn}^BAonp_Rpim?1-JxgGYeh3uK; zt||@;#9X7LCknvuZYoE|uVOhuY&qW3e|Lq^Wof*e5gL|VH>+xQ(vJ6bjeZP};ibl}2m0DVb+q~4!EfoULd1!_C&fP%2?1F&i> z?Y*ZY;phlVLdj}kbV_2FiTqHmXDl91fiUoo-;a@5gYfs2^@qc8vvwo-6P^w=VAs21 z@1tvd`?C<0ocQ4@<&5t3hIWbc+V@YMqCKWPhD&cFj;6{iENWBEUuqox%4U|< zmCgqGn-evt-4KU}D(X=Gvm?t4yxz#h{P$~%YTPp&ve)_aO+$h;7qZWMF&pH{dYTQE zA^I1p0AS&e%=LHukG^%FWCv9(Aet@174M!N?L%`){MlitseAof%!=<`^{4byCXct( zv#M*SY$jXO{2BWY%`E%f5oj#B_+rOpKFx#FW?Wfx7t&xQodd*ByCz_yy)lLC9#nm< zSzvDe7bnQ45G+!FFf9dBOZ(SJ*n3W+^DB~4HUaccrgg?Cx%iFFV`NPDA&({2&S|Aj zPF!ecKb?1fJGDCaAJ4@9J#zo~tXB#F0|tIn9t&tZU}l|%$>R(tTePWEkw)?AE(e*q z*>!DVq)DDVefkbR*NMWw7F&#DsnSs8Qm`&mR;*8gdGZ_9?u0ytF6X#QG-qafX>*1` zdkpG)TI$Zvh#DK9lxE8Om{KG)P*Bs_nr%|8-&w3bPs!AhLfCHzZs*SN-L7$4wZzUX zs!a{^9?kub)%V>cQ^Eh zl9zpTT?a5`3*nZNkgWp^Vv3twEl1HCcBHfreApv<)U-{E9~BaPsZIxA9Zd}bEw5Xn;_$aLm%%)Wen!{_`wtn~t)wG#!5-E-QSHO*h!c?r1f z!L;+Pi@zuR`LK!O`vp`o761W|1mxCTFv}Olv1rAnJO8Dfsv(M4zF}0S?tNN&*jytbGJ|Eu9@8S zrE8tX+?MMx<$gUeHs(>Te4TQlv(Bl9?aXC_yA8wO2)pikH_*c3yX-D#o=TZP0gr-jRYp4X47?gW%TV1QoA(o;#)9Q$hn(v zIQ#>~M9xel>n$06)m&BEq?LwNWC{04Yg_15mqDl#&z56A0167L&t)3iD=1UVy;N+tlCwNKfP(gocOG_FLC0{Z{Cg>|2acBh~$riK3OMV(G+-> zue`ar>F_f+_;RpT-jklQgI4{-0pc#Zj)G5YcSk2CgpsGmtiD+~J^Bcz>FAi4lxf4O z9vy`AUwcyN%W+UaY0c+KUCPGKOHhH-P5_{cCsK}mKSZq4>QdG7Q1(&@StkbL@jOnd z>-x?0u39J+LV~L>W0D)jZ}G}B#xX4I?GK4sr{@Cm_eOM*-+c;MqM)IZiFg6aLVW#* ztVt0^o|;s7exEt(}wf|I>=^@R+a*BJGgKL}Ho&si3qb(^bc5eib(&v}kIg z^rt3CCpF|*Jj=e8^2TnAB~w;+2j@OR@X~AlsJ2+DdbV5;b&*s;t(vK6GJAquJfFkX=D5;6o)De*t0@b!8t5>-I;mi^UpJ-r(08m8o_ zDH&-b^@A5tPFv09nxG|cNllVo~PG){5SaNFFWKPp0vSKpeurd9%um8C0)RcK33!*AWGK-PI z&r!Y*_AJ^C3&ax$J!7v^xQ_5`DA|${y!(O|`Qg{E!acfx^wO0-j{JNNpbw5SWOVPZ zca~)NM~$eN7YMf4nYRG-z8D0bJhPJ7jkkmnKba1fX{J^z%iny@&x=5DF)7JKne*uU z)2qu1Nw8BK5*P?gaj!(lc$rvQwneOwgv>SFnBKM#nXtaQGo%F>CsH6K>VzdG7LUA! zRRP7$=Q+Vk*Bfd~5+Z(hvr|7NSiALhecK4jg#pq{frB~(|8LJK@GJvcpYbQ!8ON#n z7gu#hkn-&)b>-~7;c1+4?|2rlx&O`iKI)UDQbM=vkNi!g>#^%`{0Oz5$&2qcR8hL& zX>6&)&BBHgVP21(PBP-|_*yXnn4?SYgaJPG-C?TQo={QjdN#G=v+8>8B)2{SZ zgf(ya@Y|>Qlr1F-SQ2-X-5?EpAP!+dak`$}@9r_$^@h+agaqFe-*$ejc1eYtJ<|8Y zW4iAB;yPwikIy!BUe>w_=*TaTIm4egX{|caMY+~n9+8=@yH*{D-bJ zr`~6K>-us@6QhF)_eYs!jZouMHct$ZOaF*#1FADi-Fos%-P6;Cm6y!6C&$NyfTYyK z>O@~j0is*x!OWlRs4EmQR%*}Dw@CzLPrACgMo9~~+bVRQ`Rx(UWZbPkjN)dYqbr7? zTrjRd=?cmm-KA-1U5Ls2@W|WvXsQPHMAV4oT!Ue6Wr5Qu6LD9wo6 zXy}!h49b6Ml(V~G-F<7v_(pC!Ui=~^AP2QqPH3cpjK2%PWI;Mz8aGstES)8s4Jwry zaxn&mhJtXoW7kDvq~6-9QXsb;i!HFqgToIO2+b%aMsw2zBe-@bXqI}GJCm-4ULkra zc;EL6ZwdVShAWZXy;XdBQQgs^es>mu+xm+li?+oN+>dYbMc-cW^%$E@BwBBVbm=Hg$Puh#Fp1wI~$B?%E7F+Y`+xi7$D+ z$l7+48x_Bji146zIL8zo3@y0V5^d2ksu&#nhEv{xjG8h$UGG@O9=@_yZSvq)hsX5y zUq;R%3V3s;5fN}|le@J=O!V|6^!Zmdd_Q)pCwk z()}>CJ-ju@@EsHP9#0O{VqaHTsftQ*HWC_~Wrkz*Ka+kCc}-Ayy}^Cd0>(d3tuZk+ zoYVh5WW#?+s^D4HTY(kSt1a0ymqtPWnlWtW4!l$Lkwh1)rQ# z7Zr3K%h`;nGumVQF}oW(0tkq+C?jLa(vMzd(-#jI7ke<9n|s~WGc@;^SbvgAQ+k#> z@Y?CC^~Ls7pQ>=_TFeW zoUU{EO(_>OnZfp59^meDu19VjFqTK({;TxpyBfH^%>OzZc`5;5SP|?t3Y+J~bgR!| zYx3wy+Wu)m1?-H}Tv*JtEmZMn4@lUh{lH?1vd!zCy9PLn8DGw+}l%$O|%k zV28ii&(qOptY&=4swEIrRmU4K&1mR-#OYtI z3;VkptihV|ednV32~8RFB1pF@;mHw^gt!QOdD`2kLTm$rV{s_DktnKYXl>E;Mwk( zj(xpS4*572VeMt_(y&`lpubpq48$waK%bH|7H2U>d2@?iykU5 zU*SOP#A5lAMqzSraF92^SyfecO~;)Q{7r85R6;W6_>g36K!Cq|JEAD0RbC($EBbX{ z@=q*9C)sEC?H0Mvm@jtv;mq9ldiuPH5`uID5P!@Y+Xr(TE#WDz+;dVM?)nTaCk*)0 zl>7XRrKSWg!8jSqQqir8a+y{5(kT!m+=2Or#%v^o$1LQB~-Z29mKmkhg64~;Y)^Cs|%xJVcz&e+Jv5SS5+0{C| zm&MPJO1P;hgHOHNLH7iTT%Jah}&Iy>mxR|8G#*(Lq+HqR( z7nLETdLqaj?0)z3nx?e`FqQO3XS=JMOpzAsZ}VN9|GM4SDDQmh#NBPsLjR#DbJ5gP z9VJ$JCN@XAxf#LGl!m(&!c{5FVOdobq+OBKLNiw}Um7~5aw69PHJTb4T0Ay=?abbq zmS+|JwnLdl^(L~_u7OwVM;s(>e#L9}2-imxQnB&{Ijjd^S!JE|G1M=+)$+0t9WZ^C z`Y=@bX6;zD*KSZ_FFEy%@X{Pui^+y_U2zeQEhnv9EF{PD;!+(Vh}}Fq=)+I)sB4!Q z>8*Kc-jpL)6($Pw#BC#yJL%LEERXizM}aw~&hR4km=*u_CcAWCRaNB@Q0FVvnMMJ){H zG;WM+uHR{Vv)D|Ti}iURNHU`Ew#r!b-B*Lph3+&R6{x_1Wzi z7UfM;Td>~h?1-u2D`D-rczKD!h57HVYLAbPC&@mqGp=vwl<9NXu8x>W4#U%`0Jaej zfLtvZ3(@2}q)~nLlNqrx!zwM-$}b6A7;62t6jHFN~4=> z;qBd0K(s%rDo+0%@fKX<--DK3S^sgWGxu8BCb8@h1s7-_C1|xynM1h zo5!X%!Kqb5SNmR&EK`yuyoJHvM9$QYPRz$))v~-hvuQnYcg3idp>-4&yY^2TKThV?M3&ddd{JqM3^TICjnID2+ zXTLZ#eW7V8$Abg(X{E#xm*^a3c1dt$!>oGybn?#gub*^^K6D!l75|vO!x=f;uVIVR z&Yn;0E+zah@R+MMDejiQ#-{awMa}6_;bTbcN)*z=!=nJt>36DJKz$We!25ou zty5QZxv0Cji1iQl^^52xspA;k)(?Mm)wv0HrN2}N52S~~}?t{*)nuzS(M2zp%hJ}UC{&Lc^ z_CMD=-Dd-7RK=R?>ur!cF_I^EWT%OG^48L^X{D_tKPJD3oBJH9~!tx`14o|?m`UDBF^U>Jk6 zO$(d+si(1vU0oBkW7D=nqOLcHMJhPL_kI!|$LAXrUGFzE&Fkm-o8$)K=_8GXGjkb# zOZ0_{b|6LKTPO>(fm2J5D)5!k*H(C1XEnptkL&b!ZBm^l%CB@iNK9V8Cg9yA(ihNM z9VteuH|-RZA|AWh`*%-Pdk>fsgim`#d4q7&UDKkxzp)B8qMsz%%(M?U$eq%!^SLVq z$7@zH_^9-dkt-Op5- za@QrG(kfjRK1n53R->0PA>*Wn;_7uAsVFZug&wb}?t_v_k6quKqk3paG&m*vd@rG> zbAIx=xJu_zZm$nYbDhe5K6Jh{i+?;Td-{0gusDvfKj3(klhX`X)_SBm32#qN7j`;` zoA%Cr;nw;Uyl@w^Es$d!-W*e=WAyk6T175uk)gN(vHoQf7>$4~-1+*-`nsn{)=DAl zfwu{8GiCYNdXfdCN?&Jw13n^?Y)yp_pr6n^-xjAoG&+j#JrBdmmhWjvmp zjm@}ztyg0m{~oM06#L9;b7KQhUhBJD$Ir*lE>Z2a`myr{)9ca8;qAq;pQC8`r9RZ1 z%hK;U*H=_jbO9%r&E5KSM%hQ^@FF|k;ct;Pp=ZwpcYAb2Bu;SgKal)VTG?2+ckMb zPQgsdD7e=Xzhd>2ZsL8W(R|$qMvMF#;rfMBv8*2AnGOyPgGiNDIE^i_GivlecYs;S zL2;pCp1=ZOEaqz8_(XM6a(jJURBvL!qI-H$o9pnmlK0X;!My1jNRQ8&csRfy zDc^e@-PZIS(~~*0Okk%S{YvNwoLfuZsj&Q`_UOMU^A=wbV%;la&vK3-l~5 zw0p_7wa%~s=Od>h0XVIY2I9FSV4@g$+$MQ`*d}+MMFCIn#=DbtA3|f*@*vTN1Y*)B z92I(b^KI%3KcY>;_$YHu*CFST2kIW6kKR5paEiuUyOGH$>7;iQckzr+|ajVu$K9jJ^Q8eGt_yJN!P}P-ig1ItuXqm zT!D#ZGy3V!g~T}Zl0BdFL9>#%<}WLrLn9eEIY(Bzmp9{E(bnGzD|@2#D+>=qgko@Z zP%Fcp#b*W4FNT7|lb@}9BANQ0cW1}v%lk2sUQZ@zQ@9RUg0jZp>)j3aX^TtPo2ElF zhk<27QmXy?Z_jur-zgXUAmYEToVmej>0FTePcZmjf#Lu5{lh$Bw+Up`&%a)IlXzp3 zW!oQCeg0Z26BH-djipeT8(FZW9mk zJJr01da8f+=}*>Xxt|@q;LaXN+mZX@wi8V2uk7r_NDDa?(Tx>av#ruLc9hhC=O)9h zi>&4TxB)f83#+K~eG<*Viqh&|$sMwb@g27vz77UGHFVs4=YvcqQL}bdT$;;VY(3)> zqfNJ?*p#KQwgS1fHMI{Pm~@%_MaUVcxK#a3NaS@-+q zsA)3BsLJJay8vLLG8g7<&%pnWhEFUIIKM=a-D={#*38yky-I6_R`6iG3T3!QDWxnN z+|nX1cE^lLq4ftCH8;CQ6xh1^1^kiluu>f!_UWkhU%51$rwUhK*LwxBeJQ!+guEE1 znd;9RwqbBKR)sj~pfaC)xR!q2JyQ_Srvj9=Gj=e{8|g(Q-`${nTiJSi-uKt1+tl+H{^G9s}nsd$aCDSFji&)3sQMg64_V zjj!K%3)|b)yk=o~(x+cBhI9(utrkcU?nN!+sjQb$SgvkX=rl`Bh-%XGs)_a}T+# z$%mr;+d=!=hj_#ba|?9+QpeW3!8xT@J5KiI?L@B92Kr!%hKcN%KAB0TZysm_wtNf> zy!+eI|8{)ICntCsjq70uG%LPj(#!9jy~F~%{(2|zN3j(S6HePzaU1%``PDXEw@B%x ze8*;&%x`4yw0IKxL6!NwUI*uawG)&`z~}M<1y& zv#(Q^OVuMCuBOi^$+v81kU&RXx19q=g8aTZxk2pC&R2y~cg%*;)Exhnma3D5PkC2( zwCqF2bO~|m)XD7JV}R}QoV$$5MwDAjNgW-DL_Dl5ddId3Iai=5*zr}X6)(~{Z9x=M zO0%m?0@l^(8rJ1B0qJM(p^ksEnevtiN)C7M>L+_YX;AqU_7heD-*$)11VCFF*hyAZ zQJL)hS?L%hS91y=19caGRt7gxuo#vIs6r6i)Ey>!B(BgWrbT?=KTgpYeC zB-Zty9_vm;fzuws{uRQ`rzuDNaQhJ|tsc^)u6G7m=RcBh75Q&B%DXExt4uJt4)^9F zEslSR{nQ*TwlN(7UCn<*S^wtYjx!Yi!Zk^V98WCej?vn&?(v?pcXWz`9pV*fzB%3N zt*x&&hOyN#I>h>9Z-M^VbKk(xQKKwu-+7;2h~(W9-8u!U=eEoy^E{7b*BQ$n@9Lm( z7suwkjw6O0@O-b1uWFU>iCxLKGiG92l?JVgCz+QxCez~^z0%D7@_W_Cq4-|}55WMo zN-#DH)*=)?A&{nB1BWwTZ2*fr-2vzG(ej~1o14s7oV17tT{W?YXp8US zj~}a{pdx`DR{8`WjijW;^RCIso}UWu0{}=`s`fjo|HhTG~msazh)LTn@SY3t%I1p-_|CEkm^t#o=D=YoR4|<>XXHHIA zbaZM7KD-#gY2BUQd*cMq-b#{?PQ0ddK;k`?SV@60N`7>Sc4Kk4Aca~1*m*dHOhb-M z-(PaHx1VZ7xsK)R9Emc};sAhX3oyj3V_trKy$_Jx>cEh20bNiwD!%DzrTv*dtB-`r z>`sD~=;QM)iIMqfNLvBpdX3*vX?H!gwAF6bqFL3hIoh;)T04vXCKDZ<$YXr<(+3JF z8<$T`E`@i4-?IXRzfl*k@}VPcc$9VIVV-5w6FZ*&E_)l+KTDp#+yx`nDecivf>zGC zzqQ&(rS_D+T465is;ma_tm)<(rY<1&i$DwflfXp64C>jnQ_Qg^^JdRKZVciOI!JXMb-~ zej*SzFzM#yYyM?@fo~{pfU}FrD`%WX&n;!cTH?iK=I|#^`1&PRrEQT;P3$nVU_45o zG1QC0x~eg^_gGV}^19;<&;f_ec^!Yjfa(WxB4X*7$?O?(n9j_^>*T0=!$dLMC5UHe_M66N-{-?|}h3u5CWKeF%z_TDVG-|oS{$?(e$ObEmQ z2bAQQ|JJ1UMQNWQCE>+N=-dnD!QA6>_t`R!LE_TPPoPK!wxwM$j3cJX8kRjX`q@-( z&11+)bOHh-Tp4I0Y-g~jfFguNYg;da*}W*5tL4DEz^sj0`QGNHB)7=@`t4}U)YzCH zs0HB#d?eaCvS?uKB6RukZ27~h=nK#6{dr6L6X0SwrTFA@RC^zgd2v6h<0i((y9Wnf zZ%FtJ<96F%K{AVc6C+rV@v9ZuPHr?wa{OG5?Fg|wgwa&FjsR}K)6?dJJ>+0lbEtT=??_I6%bXDB0N}%uql!noO38%s%-dX}8 zXSA|>*Anfd2=Gqjzmh~rP97QPMIp z%v#KPBWs6=WjQ(G9uAN@)D!HAcUkPNYb|YB=7KnZv#>;sZfNZ) zAs5yRSrL(wvz)H=e$~%Pjl+Ejf+1qN!~J*XLJ)mRzdvzO&+)tI>-$+MKP$T5I^eN4B@VYTg#@Wjiwj3bER z{D>@1@)a8vpL{jxn%0sdqFQsCyYcwg$N7m!=IfHqPHv1!#^nK1VWKVBY^S z!L*2zi0>U#nAen3$Hc<-WACw#Js01V5og?Bb$yWSyHCwJe2Mm8#@R1%V9G0=+9D*- z;M|`Zb-b~Rm1oAO*uT^twyf0C)4L+>SNd?q+X{(%Q+lI8a@qY)?jnOkI`kt*@E(B7 zW5sm*4)QPFe`V4>z=f(Te?MaES1)*@YF=5F?*IA z7Ockry1+7b;?rl(r23hT=OhXzk>_k5h??I}&MPA;=X&(?H!KF9X|!3M`v3>oknp(u z%?-N?m?jx`?-buJ9*u(i;J4Z#XX8`r;qbI-;~(Rxx2(I~qNqR6z7m3eDOd0d2=hmo z2h*@^3IorRzQnYyu|JU?4$4{ZPeS&*S5_K)&rE)gnbv9>j&2w(rss=ZLBk9;JYZ7q z$w+mOdty;qZl|4NxbWsuy7{3Af7c_XccyfDu~DQ(Jnc+0yzO@|Gs^37!kSb8&d=1u zs1n~9ePjQxq`1E+D=*L+B{r>kX3igK2o`Mjq3MbiJo>G(>Y0&9Uy+F?)?@QkvS;_7 zy;A3UopgQhFib4VDQ)xqxI$QV$lOx#)KOYyMGJ2i=1D1OgU2yv!wtp+1>{)#ZVHh0 z6>nxP9gaZj#HoCygP}`?--ykUh&Mk!V62!9HK9$fNFy;hzXL;C+cz=7sf>8(dS7;0 zBF8o7ipHb=pl8q@hg`&3Ghjm%d&iyCVllMfz6l&q#Y^(MLPW$(Q(sqSFMR2Z1(%MF zPC0(869-#(*n2{f?eg_|&SMFuyOd#oNDWtb{aI6K030IQ3`%~EhaR(&tHlfY|;6eY>72K=qJhv_4S8OJ)x>s#q+37^&_}t zP4l~IN-;EcV0C1oC3~uxL&b`QT_0z?p{32^Ll3y`7yOpnpb)CP-cx1GQR;TxS zkXy>&utpJ4ulgOon{UjQHm~L56h0WWTl!z?T)zhdFR-nXzv>=?&eEYB*QqgrV`%$v$+)!PiSm903FiC6P-)mA;Jpjcyv;GAZ?@}hFrx#Lc8)NtP zcW+%cvx&Uc(09JK*eP~0c$w+@eab$ziRT5SNaS|ut0P>b&i9|^Dd4i6`FvF3uMcnE zhN0<9Y7nI*CG=X%KNYVmN?g)*bo6^lexO@ndqTbyqe&iYlpR{(J zYYTe2az7^KGdo-7i1f+J|83f!3{)ZZs$6RhYp@rq%@!&!zcYgLDte}4+Lzn=6+O32maOL6hL};|1x% zKvT#FnnFNOGPV!Ta`7X`93cYFNSlBGfUcn>lj;Q%F#p*ItMxyi0lcfJ(w=-Y%}4vo zT?YrR?ez2)M@Oyn-A$MYSgfxR+;&#DKe)L6Weh?GAKI0q*yvHO&CLew*!FPQ{I@Rk za^bi12{OdXV!A%Qdhp?u2L*e!#G`9zl{`?JJW2)j;c3IYa;l`Amfz3SxpxWH zrB!#$o47TSxBN#z_D@9OHj6|FLnyP^h3O32X%N-b7F#H!Bca*J;yw4l8~C`o79PE; zTb%M6JJ5;Ouke;ieW~YxO78TFGoBZ=ev_?i8$MESOTMQblDo7gY&Ke!HT0~F+?K-? z{+aClKH&aG-~Z-n*?dKw4S)1vfsQrrvT49^NqkzV zcJwnJ<3B#8ZF~oAuU+!=d2u{6rnp^DC;CUZADX7j-o6qUH4#0dFA)4E4&E<&QQ18+ zKp2yMg|)QWe|Ap{YtvaWVk(sG!A`jO30>Vubrl6~W-OumbnvSd%6Rc{FA&3-IYk%$ z)14Bkhw)i=W+{|ZD2JNs<`C?|eSJ>)uk4tGNVD%fPKvjQc%jRTf}CJH{5XWoU^dFD zD_kV}r-ziAgZCf*O01|jOe~*E`-~A8`;lBV+|<-$5$fw$LQXMY&Zl;Q8~7Qry|tAH zCL&{z4+e;bi^;<=faNR*+>>LdV>~KttRwYq3G`{5C0$~t>Xgqoh1cc5d6rCS9BZmO z1g~ZDn4BGM6|T+Ww9f>UjEa`_mS&?2gpvAA3VBwoMYCt*u3PyD4qX)QXT3U0-|pD9 z_P{A|mw7K%4b4t)&-l>v#0*Qf`EF=L zr+$P21T|p+E?aOHin%D|jW=71E3@tow_wIs!}47>XbEgBerOkyZPDn5E&5C%R`9`` zKJ`xCVko$cXgak^w_$ii+&!rPoO{uE2<{#&Ch&0ps2lw&92!$0oMuhzJhD_g{a63o z-EU(><}i>qf_i9#mOu07m%txGySQ@2*MyBvC2e1qP~R#K43r1l!&%QF?+2oy_8c5N zp~uX+=wGY>r`GG_RB(-$XwsCbszW<7($J@mowI3r5rUjQ$b&Kv_!+}JJr4S>o+os= z)1~J5)$@FnmM{vf&Kh-1`5eM~)n|ifY#6{g`}NAk0A5)Fe2L0igR-tid*;DB!Pf7f z+9%?u)2WM5L~lA_EyZhVu|Q!($pHsKPabaDQ>Y*ZM7B(!)`b;=wy)%*oZu&Jt94XN zDakwblGB_N>^bZ3X{?VdXs;EOjCW0HcrI+EZ1kN_r{Uu}NXwhV=N3Z4|D;3yFEZT2 z8-e=54#6hT+P2*6J&iccvYQP`{=-C3yfGxFn#|37wFnDw^Rl`(>){bmKO!=8;s*p+ zaHb`-d=irWj`OwZ4CRJaEcKA6y06W;j!lml7pOsT6UZCN54m>`*Xfd#3IhPEvssD; z-ts#+Vuo^k>b8LZNc;%(vKlxIMdtc{5x$CvL2W7K9!$xW%}Y7C;F891?>6?d+Hn?{SwZ{4TFd+UoIuS3-{VtGP!Ur0P_u z3MQPn81)E+MGcuy#%;It;$g;J?xhe)>>sxrs_Csoitf(J7CYxOF+SN0rttvI#5d8V z@6*;>QfjN9*==Zfq`s=2vK;1L`2YW(eNy13hYdB|98gKboAgR_PI>gg@jS_sx(bP( z4`QLborG{!oDWZ1I-VYXMH#M26q5rRx*kkGbf zfLOU+F)H&!Q!n~utfE|Afv;kA-Qhgq%Eoo{ea^6l!|Sp0RapRAQYi>v-aY;?;^GjS z#s62){SY541Bgkh(N4XeLh)NFTod(DPu;=Kyc;e|LrYti{yPll5m?M3IA;t1AuuI0qWtlY4ve9fJT)HFyDYloA9BNb^>($KQF?k4CG($}4WP^4WI^Kv zt$gK~+PmFivBto})K!}R2BeYelw;FAK0Y^Eyb|5)>?XksoLxljoel8aCLJp8cx>od zWZ+qAL~a{FC6p=K-Aj02Yd(hRO#!34DU$sIEJdQUXMH$MCzQwM##_;CJMFa3^m3ns z+s|5pJU4|&W&`-i?J5+^_A?RddsXWD3q$GDX3np72e{XCu4N1EuyagI$d%&r*T#u2 z0ZN(oo{RV$Aw?399m6vg>;vL8__SYWuexG>xis=jkQza);ii#Uk+7Fpw*-+zv41;o z{%`1_(###oM_(>QSN44?pQJk(h$~-tSL3i&LfY(nV4X!ZpkYhDd{f(G{^cjXVQ~|B zX=3pSYr!zc3YdL##(V+YAn+QlhZH_gD*h8{a2w!%@H_U2 zn`LMRu5meru=2@0)OOJ z`JYrk7GwGK5$|>JFaTyt%+9_GucS@r|Ji-I$PdwKR#nz5d6_qV5(V}i`X{U17fSmN z(GvBp%dtn`<#~|~eU45}?QbFa@}Yt>0*W*u;qOoJooSU+)zo(JYgbpT2d1VrDV*CV zuh}~|Xb!?EBEgXxn3$l_^?*QN)HZKuA5aTFi3x4x<>e(}a(*BqLv0hb4|e>MS)qp+ znf2QJ4n<)p)V~DtIiD$%&Q&SXAlyva5Xv)BjgoQkU?B3!e99hka;)hVzK^Q&rPV`i zbhs|}OmrEVf4Dx!&tH@n6SIfKTLw!5fbJ|O0P#oP&BG@dzK^C#EE}sNkNS<6G)kfD ziKybL*t=ctU+GAGdH~68)p9*q`b1)|MP*-ulXSHh8`QkpRW2D8m1Z#&_arw^5ildysCQ#=@X7 zH2xZo*st($UJTf1Oi!H;QjAL+%ZB1PrnHUBq@@<9=9r(zkpG#AS3|y#ty^p1U(+4U1Zh~>{7Ag z3HNlV>gaNylsBmH;PvS0=y+m>v(8IRNy*a|Dh#}+oZ}x$PP|7EXG&hB&-wmcfnUyb zr0eOaxAK+Z^%2Lz*`Rq~2@ja>eRP~&`T~(L z*p&ayOD%|zpz<^& zw2m0XdG_PvV*2(glZP(bRV_jC0W{%_eyq5hHtFuWDUa>CdU`!iR)R}nZeDAn+_p
hxfqi7Ms2qLgQGixLZD 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 - - - - {(isAllowed) => ( - ( - - + + +
GitHub Enterprise Options
+
+ + + {(isAllowed) => ( + ( + -
- - Internet Gateway - - {gateways?.map((el) => ( - - {el.name} + onChange(undefined)} + > + Internet Gateway - ))} - -
-
-
- )} - /> - )} -
- ( - - - - )} - /> -
- - - + {gateways?.map((el) => ( + + {el.name} + + ))} + +

C8fLTdtP*R)s5fo-~GPt9L`zKFzk8X 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 z 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%0DjGQCUvTqpN#Jch7 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≠L(09)% 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
7rvFUEJ3ZK;h(5O-Qj|%&kh*GdYs& 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 diff --git a/docs/integrations/app-connections/github.mdx b/docs/integrations/app-connections/github.mdx index 8f4283ae9..9a952f815 100644 --- a/docs/integrations/app-connections/github.mdx +++ b/docs/integrations/app-connections/github.mdx @@ -94,6 +94,11 @@ Infisical supports two methods for connecting to GitHub. Select the **GitHub App** method and click **Connect to GitHub**. + + You may optionally configure GitHub Enterprise options: + - **Gateway:** The gateway connected to your private network + - **Hostname:** The hostname at which to access your GitHub Enterprise instance + ![Connect via GitHub App](/images/app-connections/github/create-github-app-method.png) From e34deb7bd0f43913bdbb5f6a23295432f03d9e9a Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 24 Jul 2025 21:48:43 -0400 Subject: [PATCH 35/79] Frontend tweak --- .../AppConnectionForm/GitHubConnectionForm.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx index 4e4c3bffd..4f2cffd44 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx @@ -46,9 +46,11 @@ type Props = { const formSchema = genericAppConnectionFieldsSchema.extend({ app: z.literal(AppConnection.GitHub), method: z.nativeEnum(GitHubConnectionMethod), - credentials: z.object({ - host: z.string().optional() - }) + credentials: z + .object({ + host: z.string().optional() + }) + .optional() }); type FormData = z.infer; @@ -92,7 +94,7 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { ); const githubHost = - formData.credentials.host && formData.credentials.host.length > 0 + formData.credentials?.host && formData.credentials.host.length > 0 ? `https://${formData.credentials.host}` : "https://github.com"; From 0f06c4c27a1426a214de6a120538f548a9a6e3e3 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 24 Jul 2025 22:25:23 -0400 Subject: [PATCH 36/79] - Add a max iteration to loop - Hide gateways on frontend if license does not allow them - Fix capitalization issue with GitHub secret sync --- .../github/github-connection-fns.ts | 25 +-- .../secret-sync/github/github-sync-fns.ts | 8 +- .../GitHubConnectionForm.tsx | 145 +++++++++--------- .../AppConnectionForm/MsSqlConnectionForm.tsx | 96 ++++++------ .../AppConnectionForm/MySqlConnectionForm.tsx | 96 ++++++------ .../OracleDBConnectionForm.tsx | 96 ++++++------ .../PostgresConnectionForm.tsx | 96 ++++++------ 7 files changed, 296 insertions(+), 266 deletions(-) diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index 5f4057bd2..b86e2ed65 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -116,6 +116,18 @@ export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) => return token; }; +function extractNextPageUrl(linkHeader: string | undefined): string | null { + if (!linkHeader) return null; + + const links = linkHeader.split(","); + const nextLink = links.find((link) => link.includes('rel="next"')); + + if (!nextLink) return null; + + const match = new RE2(/<([^>]+)>/).exec(nextLink); + return match ? match[1] : null; +} + export const makePaginatedGitHubRequest = async ( appConnection: TGitHubConnection, gatewayService: Pick, @@ -128,8 +140,9 @@ export const makePaginatedGitHubRequest = async ( method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection); let url: string | null = `https://api.${credentials.host || "github.com"}${path}`; let results: T[] = []; + let i = 0; - while (url) { + while (url && i < 1000) { // eslint-disable-next-line no-await-in-loop const response: AxiosResponse = await requestWithGitHubGateway(appConnection, gatewayService, { url, @@ -144,14 +157,8 @@ export const makePaginatedGitHubRequest = async ( const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]); results = results.concat(items); - const linkHeader = response.headers.link as string | undefined; - const nextLink = - typeof linkHeader === "string" ? linkHeader.split(",").find((s) => s.includes('rel="next"')) : undefined; - if (nextLink) { - url = new RE2(/<(.+)>/).exec(nextLink)?.[1] || null; - } else { - url = null; - } + url = extractNextPageUrl(response.headers.link as string | undefined); + i += 1; } return results; diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index 580d77bca..022490da6 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -171,9 +171,11 @@ const putSecret = async ( export const GithubSyncFns = { syncSecrets: async ( secretSync: TGitHubSyncWithCredentials, - secretMap: TSecretMap, + ogSecretMap: TSecretMap, gatewayService: Pick ) => { + const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v])); + switch (secretSync.destinationConfig.scope) { case GitHubSyncScope.Organization: if (Object.values(secretMap).length > 1000) { @@ -252,9 +254,11 @@ export const GithubSyncFns = { }, removeSecrets: async ( secretSync: TGitHubSyncWithCredentials, - secretMap: TSecretMap, + ogSecretMap: TSecretMap, gatewayService: Pick ) => { + const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v])); + const { connection } = secretSync; const token = connection.method === GitHubConnectionMethod.OAuth diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx index 4f2cffd44..eb93993af 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/GitHubConnectionForm.tsx @@ -20,6 +20,7 @@ import { SelectItem, Tooltip } from "@app/components/v2"; +import { useSubscription } from "@app/context"; import { OrgGatewayPermissionActions, OrgPermissionSubjects @@ -80,6 +81,7 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { formState: { isSubmitting, isDirty } } = form; + const { subscription } = useSubscription(); const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const selectedMethod = watch("method"); @@ -173,80 +175,81 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => { )} /> - - - -

+ + + )} + /> + )} + + ( + + + + )} + /> + + + + )}
+ + + )} + /> + )} + + )} { formState: { isSubmitting, isDirty } } = form; + const { subscription } = useSubscription(); const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); @@ -96,55 +98,57 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } - - {(isAllowed) => ( - ( - - + {(isAllowed) => ( + ( + -
- - Internet Gateway - - {gateways?.map((el) => ( - - {el.name} + onChange(undefined)} + > + Internet Gateway - ))} - -
-
-
- )} - /> - )} -
+ {gateways?.map((el) => ( + + {el.name} + + ))} + +
+ + + )} + /> + )} + + )} { formState: { isSubmitting, isDirty } } = form; + const { subscription } = useSubscription(); const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); @@ -96,55 +98,57 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } - - {(isAllowed) => ( - ( - - + {(isAllowed) => ( + ( + -
- - Internet Gateway - - {gateways?.map((el) => ( - - {el.name} + onChange(undefined)} + > + Internet Gateway - ))} - -
-
-
- )} - /> - )} -
+ {gateways?.map((el) => ( + + {el.name} + + ))} + +
+ + + )} + /> + )} + + )} { formState: { isSubmitting, isDirty } } = form; + const { subscription } = useSubscription(); const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false; const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); @@ -96,55 +98,57 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } - - {(isAllowed) => ( - ( - - + {(isAllowed) => ( + ( + -
- - Internet Gateway - - {gateways?.map((el) => ( - - {el.name} + onChange(undefined)} + > + Internet Gateway - ))} - -
-
-
- )} - /> - )} -
+ {gateways?.map((el) => ( + + {el.name} + + ))} + +
+ + + )} + /> + )} + + )} Date: Thu, 24 Jul 2025 22:53:49 -0400 Subject: [PATCH 37/79] Validate hostname --- .../services/app-connection/github/github-connection-fns.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index b86e2ed65..b38b9406b 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -10,6 +10,7 @@ import { request as httpRequest } from "@app/lib/config/request"; import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { logger } from "@app/lib/logger"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; @@ -30,7 +31,7 @@ export const getGitHubConnectionListItem = () => { }; export const requestWithGitHubGateway = async ( - appConnection: { gatewayId?: string | null; credentials: { host?: string } }, + appConnection: { gatewayId?: string | null }, gatewayService: Pick, requestConfig: AxiosRequestConfig ): Promise> => { @@ -43,6 +44,8 @@ export const requestWithGitHubGateway = async ( const url = new URL(requestConfig.url as string); + await blockLocalAndPrivateIpAddresses(url.toString()); + const [targetHost] = await verifyHostInputValidity(url.host, true); const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); From 7365f60835b971261492c2fe945e45c69150ad68 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 01:23:01 -0300 Subject: [PATCH 38/79] Small code improvements --- .../azure-client-secrets-connection-fns.ts | 4 ++-- .../azure-client-secrets-connection-schemas.ts | 14 ++++++++------ .../azure-client-secrets-connection-types.ts | 6 +++--- .../AzureClientSecretsConnectionForm.tsx | 6 ++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index fa2563078..a28217320 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -17,7 +17,7 @@ import { AppConnection } from "../app-connection-enums"; import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums"; import { ExchangeCodeAzureResponse, - TAzureClientSecretsConnectionAccessTokenCredentials, + TAzureClientSecretsConnectionClientSecretCredentials, TAzureClientSecretsConnectionConfig, TAzureClientSecretsConnectionCredentials } from "./azure-client-secrets-connection-types"; @@ -101,7 +101,7 @@ export const getAzureConnectionAccessToken = async ( orgId: appConnection.orgId, kmsService, encryptedCredentials: appConnection.encryptedCredentials - })) as TAzureClientSecretsConnectionAccessTokenCredentials; + })) as TAzureClientSecretsConnectionClientSecretCredentials; const { accessToken, expiresAt, clientId, clientSecret, tenantId } = accessTokenCredentials; if (accessToken && expiresAt && expiresAt > currentTime + 300000) { return accessToken; diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts index 4d2c486d7..41c9a1d36 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts @@ -26,16 +26,18 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object expiresAt: z.number() }); -export const AzureClientSecretsConnectionAccessTokenInputCredentialsSchema = z.object({ +export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z.object({ clientId: z .string() .trim() .min(1, "Client ID required") + .max(50, "Client ID must be at most 50 characters long") .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientId), clientSecret: z .string() .trim() .min(1, "Client Secret required") + .max(50, "Client Secret must be at most 50 characters long") .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientSecret), tenantId: z .string() @@ -44,7 +46,7 @@ export const AzureClientSecretsConnectionAccessTokenInputCredentialsSchema = z.o .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId) }); -export const AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema = z.object({ +export const AzureClientSecretsConnectionClientSecretOutputCredentialsSchema = z.object({ clientId: z.string(), clientSecret: z.string(), tenantId: z.string(), @@ -65,7 +67,7 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin method: z .literal(AzureClientSecretsConnectionMethod.ClientSecret) .describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method), - credentials: AzureClientSecretsConnectionAccessTokenInputCredentialsSchema.describe( + credentials: AzureClientSecretsConnectionClientSecretInputCredentialsSchema.describe( AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials ) }) @@ -80,7 +82,7 @@ export const UpdateAzureClientSecretsConnectionSchema = z credentials: z .union([ AzureClientSecretsConnectionOAuthInputCredentialsSchema, - AzureClientSecretsConnectionAccessTokenInputCredentialsSchema + AzureClientSecretsConnectionClientSecretInputCredentialsSchema ]) .optional() .describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials) @@ -100,7 +102,7 @@ export const AzureClientSecretsConnectionSchema = z.intersection( }), z.object({ method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret), - credentials: AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema + credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema }) ]) ); @@ -114,7 +116,7 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion( }), BaseAzureClientSecretsConnectionSchema.extend({ method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret), - credentials: AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema.pick({ + credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({ clientId: true, tenantId: true }) diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts index f6aa932d7..1ad5a3411 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts @@ -4,7 +4,7 @@ import { DiscriminativePick } from "@app/lib/types"; import { AppConnection } from "../app-connection-enums"; import { - AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema, + AzureClientSecretsConnectionClientSecretOutputCredentialsSchema, AzureClientSecretsConnectionOAuthOutputCredentialsSchema, AzureClientSecretsConnectionSchema, CreateAzureClientSecretsConnectionSchema, @@ -31,8 +31,8 @@ export type TAzureClientSecretsConnectionCredentials = z.infer< typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema >; -export type TAzureClientSecretsConnectionAccessTokenCredentials = z.infer< - typeof AzureClientSecretsConnectionAccessTokenOutputCredentialsSchema +export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer< + typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema >; export interface ExchangeCodeAzureResponse { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx index 927189e6d..6aa6ee63c 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx @@ -205,7 +205,7 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr > { field.onChange(e.target.value); setValue("credentials.tenantId", e.target.value); @@ -223,12 +223,11 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr control={control} render={({ field, fieldState: { error } }) => ( - + )} /> @@ -237,7 +236,6 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr control={control} render={({ field, fieldState: { error } }) => ( Date: Fri, 25 Jul 2025 01:37:25 -0300 Subject: [PATCH 39/79] Minor fixes on policies multi env migration --- ...2152841_add-policies-environments-table.ts | 11 +++++---- .../access-approval-policy-service.ts | 23 +++++++++++-------- .../secret-approval-policy-service.ts | 23 +++++++++++-------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts index c8ee3d524..57ec13203 100644 --- a/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts +++ b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts @@ -12,11 +12,13 @@ export async function up(knex: Knex): Promise { t.uuid("policyId").notNullable(); t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); t.uuid("envId").notNullable(); - t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + t.foreign("envId").references("id").inTable(TableName.Environment); t.timestamps(true, true, true); t.unique(["policyId", "envId"]); }); + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment); + const existingAccessApprovalPolicies = await knex(TableName.AccessApprovalPolicy) .select(selectAllTableCols(TableName.AccessApprovalPolicy)) .whereNotNull(`${TableName.AccessApprovalPolicy}.envId`); @@ -36,11 +38,13 @@ export async function up(knex: Knex): Promise { t.uuid("policyId").notNullable(); t.foreign("policyId").references("id").inTable(TableName.SecretApprovalPolicy).onDelete("CASCADE"); t.uuid("envId").notNullable(); - t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + t.foreign("envId").references("id").inTable(TableName.Environment); t.timestamps(true, true, true); t.unique(["policyId", "envId"]); }); + await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment); + const existingSecretApprovalPolicies = await knex(TableName.SecretApprovalPolicy) .select(selectAllTableCols(TableName.SecretApprovalPolicy)) .whereNotNull(`${TableName.SecretApprovalPolicy}.envId`); @@ -68,9 +72,6 @@ export async function up(knex: Knex): Promise { // Add the new foreign key constraint with ON DELETE SET NULL t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL"); }); - - await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment); - await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment); } export async function down(knex: Knex): Promise { diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index d6187d418..0b3c4e128 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -354,16 +354,19 @@ export const accessApprovalPolicyServiceFactory = ({ envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: accessApprovalPolicy.projectId }); } - if ( - await $policyExists({ - envIds: envs.map((env) => env.id), - secretPath: secretPath || accessApprovalPolicy.secretPath, - policyId: accessApprovalPolicy.id - }) - ) { - throw new BadRequestError({ - message: `A policy for secret path '${secretPath}' already exists` - }); + for (const env of envs) { + if ( + // eslint-disable-next-line no-await-in-loop + await $policyExists({ + envId: env.id, + secretPath: secretPath || accessApprovalPolicy.secretPath, + policyId: accessApprovalPolicy.id + }) + ) { + throw new BadRequestError({ + message: `A policy for secret path '${secretPath || accessApprovalPolicy.secretPath}' already exists in environment '${env.slug}'` + }); + } } const { permission } = await permissionService.getProjectPermission({ diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 2bff6f440..96757dc22 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -315,16 +315,19 @@ export const secretApprovalPolicyServiceFactory = ({ ) { envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: secretApprovalPolicy.projectId }); } - if ( - await $policyExists({ - envIds: envs.map((env) => env.id), - secretPath: secretPath || secretApprovalPolicy.secretPath, - policyId: secretApprovalPolicy.id - }) - ) { - throw new BadRequestError({ - message: `A policy for secret path '${secretPath}' already exists` - }); + for (const env of envs) { + if ( + // eslint-disable-next-line no-await-in-loop + await $policyExists({ + envId: env.id, + secretPath: secretPath || secretApprovalPolicy.secretPath, + policyId: secretApprovalPolicy.id + }) + ) { + throw new BadRequestError({ + message: `A policy for secret path '${secretPath || secretApprovalPolicy.secretPath}' already exists in environment '${env.slug}'` + }); + } } const { permission } = await permissionService.getProjectPermission({ From 418aca8af067847631086e811f1a04fcac45bd7b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 25 Jul 2025 19:50:28 +0400 Subject: [PATCH 40/79] feat(secret-sync/render): auto redeploy on sync --- backend/src/lib/api-docs/constants.ts | 4 ++ .../secret-sync/render/render-sync-fns.ts | 30 ++++++++++++++ .../secret-sync/render/render-sync-schemas.ts | 16 ++++++-- .../RenderSyncOptionsFields.tsx | 40 +++++++++++++++++++ .../SecretSyncOptionsFields.tsx | 5 ++- .../RenderSyncReviewFields.tsx | 21 ++++++++++ .../SecretSyncReviewFields.tsx | 3 +- .../schemas/render-sync-destination-schema.ts | 6 ++- .../src/hooks/api/secretSyncs/render-sync.ts | 4 ++ .../RenderSyncOptionsSection.tsx | 27 +++++++++++++ .../SecretSyncOptionsSection.tsx | 5 ++- 11 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx create mode 100644 frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b6c00985a..318c085a1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2373,6 +2373,10 @@ export const SecretSyncs = { keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.", tags: "Optional tags to add to secrets synced by Infisical.", syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.` + }, + RENDER: { + autoRedeployServices: + "Whether Infisical should automatically redeploy the configured Render service upon secret changes." } }, DESTINATION_CONFIG: { diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts index 36e97e620..8af3653ca 100644 --- a/backend/src/services/secret-sync/render/render-sync-fns.ts +++ b/backend/src/services/secret-sync/render/render-sync-fns.ts @@ -97,6 +97,28 @@ const batchUpdateEnvironmentSecrets = async ( ); }; +const redeployService = async (secretSync: TRenderSyncWithCredentials) => { + const { + destinationConfig, + connection: { + credentials: { apiKey } + } + } = secretSync; + + await makeRequestWithRetry(() => + request.post( + `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`, + {}, + { + headers: { + Authorization: `Bearer ${apiKey}`, + "Accept-Encoding": "application/json" + } + } + ) + ); +}; + export const RenderSyncFns = { syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => { const renderSecrets = await getRenderEnvironmentSecrets(secretSync); @@ -131,6 +153,10 @@ export const RenderSyncFns = { } await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars); + + if (secretSync.syncOptions.autoRedeployServices) { + await redeployService(secretSync); + } }, getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise => { @@ -151,5 +177,9 @@ export const RenderSyncFns = { } } await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars); + + if (secretSync.syncOptions.autoRedeployServices) { + await redeployService(secretSync); + } } }; diff --git a/backend/src/services/secret-sync/render/render-sync-schemas.ts b/backend/src/services/secret-sync/render/render-sync-schemas.ts index 77414c17c..0d6e93987 100644 --- a/backend/src/services/secret-sync/render/render-sync-schemas.ts +++ b/backend/src/services/secret-sync/render/render-sync-schemas.ts @@ -20,23 +20,33 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [ }) ]); +const RenderSyncOptionsSchema = z.object({ + autoRedeployServices: z.boolean().optional().describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.RENDER.autoRedeployServices) +}); + const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; -export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({ +export const RenderSyncSchema = BaseSecretSyncSchema( + SecretSync.Render, + RenderSyncOptionsConfig, + RenderSyncOptionsSchema +).extend({ destination: z.literal(SecretSync.Render), destinationConfig: RenderSyncDestinationConfigSchema }); export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema( SecretSync.Render, - RenderSyncOptionsConfig + RenderSyncOptionsConfig, + RenderSyncOptionsSchema ).extend({ destinationConfig: RenderSyncDestinationConfigSchema }); export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema( SecretSync.Render, - RenderSyncOptionsConfig + RenderSyncOptionsConfig, + RenderSyncOptionsSchema ).extend({ destinationConfig: RenderSyncDestinationConfigSchema.optional() }); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx new file mode 100644 index 000000000..6d4173bd0 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/RenderSyncOptionsFields.tsx @@ -0,0 +1,40 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { FormControl, Switch, Tooltip } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const RenderSyncOptionsFields = () => { + const { control } = useFormContext(); + + return ( + ( + + + Auto Redeploy Services On Sync + If enabled, services will be automatically redeployed upon secret changes.

+ } + > + +
+
+
+ )} + /> + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 50ea46ac0..cb7a40559 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -14,6 +14,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields"; import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields"; +import { RenderSyncOptionsFields } from "./RenderSyncOptionsFields"; type Props = { hideInitialSync?: boolean; @@ -38,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.AWSSecretsManager: AdditionalSyncOptionsFieldsComponent = ; break; + case SecretSync.Render: + AdditionalSyncOptionsFieldsComponent = ; + break; case SecretSync.GitHub: case SecretSync.GCPSecretManager: case SecretSync.AzureKeyVault: @@ -54,7 +58,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.OnePass: case SecretSync.OCIVault: case SecretSync.Heroku: - case SecretSync.Render: case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx index becc46c1d..15f5b6625 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx @@ -2,8 +2,29 @@ import { useFormContext } from "react-hook-form"; import { GenericFieldLabel } from "@app/components/secret-syncs"; import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { Badge } from "@app/components/v2"; import { SecretSync } from "@app/hooks/api/secretSyncs"; +export const RenderSyncOptionsReviewFields = () => { + const { watch } = useFormContext(); + + const [{ autoRedeployServices }] = watch(["syncOptions"]); + + return ( +
+ {autoRedeployServices ? ( + + Enabled + + ) : ( + + Disabled + + )} +
+ ); +}; + export const RenderSyncReviewFields = () => { const { watch } = useFormContext(); const serviceName = watch("destinationConfig.serviceName"); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index c1194a4c1..5ee53f2e3 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -35,7 +35,7 @@ import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields"; import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields"; import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields"; -import { RenderSyncReviewFields } from "./RenderSyncReviewFields"; +import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields"; import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; @@ -121,6 +121,7 @@ export const SecretSyncReviewFields = () => { break; case SecretSync.Render: DestinationFieldsComponent = ; + AdditionalSyncOptionsFieldsComponent = ; break; case SecretSync.Flyio: DestinationFieldsComponent = ; diff --git a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts index 16b213421..da81e121d 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts @@ -4,7 +4,11 @@ import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas import { SecretSync } from "@app/hooks/api/secretSyncs"; import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync"; -export const RenderSyncDestinationSchema = BaseSecretSyncSchema().merge( +export const RenderSyncDestinationSchema = BaseSecretSyncSchema( + z.object({ + autoRedeployServices: z.boolean().optional() + }) +).merge( z.object({ destination: z.literal(SecretSync.Render), destinationConfig: z.discriminatedUnion("scope", [ diff --git a/frontend/src/hooks/api/secretSyncs/render-sync.ts b/frontend/src/hooks/api/secretSyncs/render-sync.ts index ecac7d077..61aecc53f 100644 --- a/frontend/src/hooks/api/secretSyncs/render-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/render-sync.ts @@ -16,6 +16,10 @@ export type TRenderSync = TRootSecretSync & { name: string; id: string; }; + + syncOptions: { + autoRedeployServices?: boolean; + }; }; export enum RenderSyncScope { diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx new file mode 100644 index 000000000..3dd3d2fd5 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx @@ -0,0 +1,27 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { Badge } from "@app/components/v2"; +import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; + +type Props = { + secretSync: TRenderSync; +}; + +export const RenderSyncOptionsSection = ({ secretSync }: Props) => { + const { + syncOptions: { autoRedeployServices } + } = secretSync; + + return ( +
+ {autoRedeployServices ? ( + + Enabled + + ) : ( + + Disabled + + )} +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 843e02f63..32c46fca2 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -13,6 +13,7 @@ import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; import { AwsParameterStoreSyncOptionsSection } from "./AwsParameterStoreSyncOptionsSection"; import { AwsSecretsManagerSyncOptionsSection } from "./AwsSecretsManagerSyncOptionsSection"; +import { RenderSyncOptionsSection } from "./RenderSyncOptionsSection"; type Props = { secretSync: TSecretSync; @@ -40,6 +41,9 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = ); break; + case SecretSync.Render: + AdditionalSyncOptionsComponent = ; + break; case SecretSync.GitHub: case SecretSync.GCPSecretManager: case SecretSync.AzureKeyVault: @@ -56,7 +60,6 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.OCIVault: case SecretSync.OnePass: case SecretSync.Heroku: - case SecretSync.Render: case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: From 11ca76cccaa8a98beef9b882cfae78514fdaf0f2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 25 Jul 2025 20:05:20 +0400 Subject: [PATCH 41/79] fix: restructure and requested changes --- .../secret-sync/render/render-sync-fns.ts | 2 +- .../RenderSyncFields.tsx | 2 +- .../schemas/render-sync-destination-schema.ts | 2 +- frontend/src/helpers/secretSyncs.ts | 2 +- .../src/hooks/api/secretSyncs/types/index.ts | 2 +- .../api/secretSyncs/{ => types}/render-sync.ts | 4 ++-- .../RenderSyncDestinationCol.tsx | 2 +- .../RenderSyncDestinationSection.tsx | 2 +- .../RenderSyncOptionsSection.tsx | 18 ++++++------------ 9 files changed, 15 insertions(+), 21 deletions(-) rename frontend/src/hooks/api/secretSyncs/{ => types}/render-sync.ts (82%) diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts index 8af3653ca..71347f998 100644 --- a/backend/src/services/secret-sync/render/render-sync-fns.ts +++ b/backend/src/services/secret-sync/render/render-sync-fns.ts @@ -112,7 +112,7 @@ const redeployService = async (secretSync: TRenderSyncWithCredentials) => { { headers: { Authorization: `Bearer ${apiKey}`, - "Accept-Encoding": "application/json" + Accept: "application/json" } } ) diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx index b5cb407cb..3d3f8df93 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RenderSyncFields.tsx @@ -9,7 +9,7 @@ import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render"; import { SecretSync } from "@app/hooks/api/secretSyncs"; -import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync"; +import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync"; import { TSecretSyncForm } from "../schemas"; diff --git a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts index da81e121d..83e5347eb 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; import { SecretSync } from "@app/hooks/api/secretSyncs"; -import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync"; +import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync"; export const RenderSyncDestinationSchema = BaseSecretSyncSchema( z.object({ diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 0eb41e119..b4c0df352 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -4,9 +4,9 @@ import { SecretSyncImportBehavior, SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs"; -import { RenderSyncScope } from "@app/hooks/api/secretSyncs/render-sync"; import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync"; import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync"; +import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync"; export const SECRET_SYNC_MAP: Record = { [SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" }, diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 7af01765e..2ab76341b 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -1,7 +1,6 @@ import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs"; import { DiscriminativePick } from "@app/types"; -import { TRenderSync } from "../render-sync"; import { TOnePassSync } from "./1password-sync"; import { TAwsParameterStoreSync } from "./aws-parameter-store-sync"; import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync"; @@ -24,6 +23,7 @@ import { THerokuSync } from "./heroku-sync"; import { THumanitecSync } from "./humanitec-sync"; import { TOCIVaultSync } from "./oci-vault-sync"; import { TRailwaySync } from "./railway-sync"; +import { TRenderSync } from "./render-sync"; import { TSupabaseSync } from "./supabase"; import { TTeamCitySync } from "./teamcity-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync"; diff --git a/frontend/src/hooks/api/secretSyncs/render-sync.ts b/frontend/src/hooks/api/secretSyncs/types/render-sync.ts similarity index 82% rename from frontend/src/hooks/api/secretSyncs/render-sync.ts rename to frontend/src/hooks/api/secretSyncs/types/render-sync.ts index 61aecc53f..3d66de623 100644 --- a/frontend/src/hooks/api/secretSyncs/render-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/render-sync.ts @@ -1,6 +1,6 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { SecretSync } from "@app/hooks/api/secretSyncs"; -import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; +import { RootSyncOptions, TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; export type TRenderSync = TRootSecretSync & { destination: SecretSync.Render; @@ -17,7 +17,7 @@ export type TRenderSync = TRootSecretSync & { id: string; }; - syncOptions: { + syncOptions: RootSyncOptions & { autoRedeployServices?: boolean; }; }; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx index 43e9e35d2..00fcfe264 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/RenderSyncDestinationCol.tsx @@ -1,5 +1,5 @@ import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render"; -import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; +import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync"; import { getSecretSyncDestinationColValues } from "../helpers"; import { SecretSyncTableCell } from "../SecretSyncTableCell"; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx index b661caf00..eda37a9cf 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RenderSyncDestinationSection.tsx @@ -1,6 +1,6 @@ import { GenericFieldLabel } from "@app/components/secret-syncs"; import { useRenderConnectionListServices } from "@app/hooks/api/appConnections/render"; -import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; +import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync"; type Props = { secretSync: TRenderSync; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx index 3dd3d2fd5..b23b3298b 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/RenderSyncOptionsSection.tsx @@ -1,6 +1,6 @@ import { GenericFieldLabel } from "@app/components/secret-syncs"; import { Badge } from "@app/components/v2"; -import { TRenderSync } from "@app/hooks/api/secretSyncs/render-sync"; +import { TRenderSync } from "@app/hooks/api/secretSyncs/types/render-sync"; type Props = { secretSync: TRenderSync; @@ -12,16 +12,10 @@ export const RenderSyncOptionsSection = ({ secretSync }: Props) => { } = secretSync; return ( -
- {autoRedeployServices ? ( - - Enabled - - ) : ( - - Disabled - - )} -
+ + + {autoRedeployServices ? "Enabled" : "Disabled"} + + ); }; From 4afc7a19816512830f7c8cd87851ad6545100c86 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 13:06:29 -0300 Subject: [PATCH 42/79] Add manual migration to secret imports rework --- ...25144940_fix-secret-reminders-migration.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts new file mode 100644 index 000000000..81e8ccf31 --- /dev/null +++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts @@ -0,0 +1,92 @@ +/* eslint-disable no-await-in-loop */ +import { Knex } from "knex"; + +import { chunkArray } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; + +import { TableName } from "../schemas"; +import { TReminders, TRemindersInsert } from "../schemas/reminders"; + +export async function up(knex: Knex): Promise { + logger.info("Initializing secret reminders migration"); + const hasReminderTable = await knex.schema.hasTable(TableName.Reminder); + + if (hasReminderTable) { + const secretsWithLatestVersions = await knex(TableName.SecretV2) + .whereNotNull(`${TableName.SecretV2}.reminderRepeatDays`) + .whereRaw(`"${TableName.SecretV2}"."reminderRepeatDays" > 0`) + .innerJoin(TableName.SecretVersionV2, (qb) => { + void qb + .on(`${TableName.SecretVersionV2}.secretId`, "=", `${TableName.SecretV2}.id`) + .andOn(`${TableName.SecretVersionV2}.reminderRepeatDays`, "=", `${TableName.SecretV2}.reminderRepeatDays`); + }) + .whereIn([`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretVersionV2}.version`], (qb) => { + void qb + .select(["secretId", knex.raw("MAX(version) as version")]) + .from(`${TableName.SecretVersionV2} as v2`) + .whereNotNull("v2.reminderRepeatDays") + .whereRaw(`"v2"."reminderRepeatDays" > 0`) + .groupBy("v2.secretId"); + }) + .select( + knex.ref("id").withSchema(TableName.SecretV2).as("secretId"), + knex.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("reminderRepeatDays"), + knex.ref("reminderNote").withSchema(TableName.SecretV2).as("reminderNote"), + knex.ref("createdAt").withSchema(TableName.SecretVersionV2).as("createdAt") + ); + + logger.info(`Found ${secretsWithLatestVersions.length} reminders to migrate`); + + const reminderInserts: TRemindersInsert[] = []; + if (secretsWithLatestVersions.length > 0) { + secretsWithLatestVersions.forEach((secret) => { + if (!secret.reminderRepeatDays) return; + const nextReminderDate = new Date(secret.createdAt); + nextReminderDate.setDate(nextReminderDate.getDate() + secret.reminderRepeatDays); + + reminderInserts.push({ + secretId: secret.secretId, + message: secret.reminderNote, + repeatDays: secret.reminderRepeatDays, + nextReminderDate + }); + }); + + const commitBatches = chunkArray(reminderInserts, 9000); + for (const commitBatch of commitBatches) { + const insertedReminders = (await knex + .batchInsert(TableName.Reminder, commitBatch) + .returning("*")) as TReminders[]; + + const insertedReminderSecretIds = insertedReminders.map((reminder) => reminder.secretId).filter(Boolean); + + const recipients = await knex(TableName.SecretReminderRecipients) + .whereRaw(`??.?? IN (${insertedReminderSecretIds.map(() => "?").join(",")})`, [ + TableName.SecretReminderRecipients, + "secretId", + ...insertedReminderSecretIds + ]) + .select( + knex.ref("userId").withSchema(TableName.SecretReminderRecipients).as("userId"), + knex.ref("secretId").withSchema(TableName.SecretReminderRecipients).as("secretId") + ); + const reminderRecipients = recipients.map((recipient) => ({ + reminderId: insertedReminders.find((reminder) => reminder.secretId === recipient.secretId)?.id, + userId: recipient.userId + })); + + const filteredRecipients = reminderRecipients.filter((recipient) => !!recipient.reminderId); + await knex.batchInsert(TableName.ReminderRecipient, filteredRecipients); + } + logger.info(`Successfully migrated ${reminderInserts.length} secret reminders`); + } + + logger.info("Secret reminders migration completed"); + } else { + logger.warn("Reminder table does not exist, skipping migration"); + } +} + +export async function down(): Promise { + logger.info("Rollback not implemented for secret reminders fix migration"); +} From cd718488000e1c52309bac9d0d33e7657c387baf Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 13:10:54 -0300 Subject: [PATCH 43/79] Avoid migrating existing reminders --- .../20250725144940_fix-secret-reminders-migration.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts index 81e8ccf31..35612f2d7 100644 --- a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts +++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts @@ -28,6 +28,10 @@ export async function up(knex: Knex): Promise { .whereRaw(`"v2"."reminderRepeatDays" > 0`) .groupBy("v2.secretId"); }) + // Add LEFT JOIN with Reminder table to check for existing reminders + .leftJoin(TableName.Reminder, `${TableName.Reminder}.secretId`, `${TableName.SecretV2}.id`) + // Only include secrets that don't already have reminders + .whereNull(`${TableName.Reminder}.secretId`) .select( knex.ref("id").withSchema(TableName.SecretV2).as("secretId"), knex.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("reminderRepeatDays"), From af32948a05b472ba0cab4547e30796604922a75f Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 13:35:06 -0300 Subject: [PATCH 44/79] Minor improvements on reminders migration --- .../20250725144940_fix-secret-reminders-migration.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts index 35612f2d7..9a0394bf9 100644 --- a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts +++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts @@ -22,10 +22,12 @@ export async function up(knex: Knex): Promise { }) .whereIn([`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretVersionV2}.version`], (qb) => { void qb - .select(["secretId", knex.raw("MAX(version) as version")]) + .select(["v2.secretId", knex.raw("MIN(v2.version) as version")]) .from(`${TableName.SecretVersionV2} as v2`) + .innerJoin(`${TableName.SecretV2} as s2`, "v2.secretId", "s2.id") + .whereRaw(`v2."reminderRepeatDays" = s2."reminderRepeatDays"`) .whereNotNull("v2.reminderRepeatDays") - .whereRaw(`"v2"."reminderRepeatDays" > 0`) + .whereRaw(`v2."reminderRepeatDays" > 0`) .groupBy("v2.secretId"); }) // Add LEFT JOIN with Reminder table to check for existing reminders @@ -56,7 +58,7 @@ export async function up(knex: Knex): Promise { }); }); - const commitBatches = chunkArray(reminderInserts, 9000); + const commitBatches = chunkArray(reminderInserts, 2000); for (const commitBatch of commitBatches) { const insertedReminders = (await knex .batchInsert(TableName.Reminder, commitBatch) @@ -79,7 +81,7 @@ export async function up(knex: Knex): Promise { userId: recipient.userId })); - const filteredRecipients = reminderRecipients.filter((recipient) => !!recipient.reminderId); + const filteredRecipients = reminderRecipients.filter((recipient) => Boolean(recipient.reminderId)); await knex.batchInsert(TableName.ReminderRecipient, filteredRecipients); } logger.info(`Successfully migrated ${reminderInserts.length} secret reminders`); From 7ce11cde9569c5c9dd3ac10144671d0c30c17dee Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 14:47:57 -0300 Subject: [PATCH 45/79] Add cycle logic to next reminder migration --- ...250725144940_fix-secret-reminders-migration.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts index 9a0394bf9..b720c97ae 100644 --- a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts +++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts @@ -47,9 +47,22 @@ export async function up(knex: Knex): Promise { if (secretsWithLatestVersions.length > 0) { secretsWithLatestVersions.forEach((secret) => { if (!secret.reminderRepeatDays) return; - const nextReminderDate = new Date(secret.createdAt); + + const now = new Date(); + const createdAt = new Date(secret.createdAt); + let nextReminderDate = new Date(createdAt); nextReminderDate.setDate(nextReminderDate.getDate() + secret.reminderRepeatDays); + // If the next reminder date is in the past, calculate the proper next occurrence + if (nextReminderDate < now) { + const daysSinceCreation = Math.floor((now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)); + const daysIntoCurrentCycle = daysSinceCreation % secret.reminderRepeatDays; + const daysUntilNextReminder = secret.reminderRepeatDays - daysIntoCurrentCycle; + + nextReminderDate = new Date(now); + nextReminderDate.setDate(now.getDate() + daysUntilNextReminder); + } + reminderInserts.push({ secretId: secret.secretId, message: secret.reminderNote, From d0ffae2c109c23e3138f89dc7d365b02a03212be Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 14:53:46 -0300 Subject: [PATCH 46/79] Add uuid validation to Azure client secrets --- .../azure-client-secrets-connection-schemas.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts index 41c9a1d36..d9f178a06 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts @@ -29,6 +29,7 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z.object({ clientId: z .string() + .uuid() .trim() .min(1, "Client ID required") .max(50, "Client ID must be at most 50 characters long") @@ -41,6 +42,7 @@ export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z. .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientSecret), tenantId: z .string() + .uuid() .trim() .min(1, "Tenant ID required") .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId) From 72ee468208f8d1bf90c4c9020a7e1aa1037123df Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 15:20:23 -0300 Subject: [PATCH 47/79] Remove previous queue running the migration --- backend/src/services/reminder/reminder-queue.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/src/services/reminder/reminder-queue.ts b/backend/src/services/reminder/reminder-queue.ts index 1487a63f3..c038734bd 100644 --- a/backend/src/services/reminder/reminder-queue.ts +++ b/backend/src/services/reminder/reminder-queue.ts @@ -173,12 +173,6 @@ export const dailyReminderQueueServiceFactory = ({ { pattern: "0 */1 * * *", utc: true }, QueueName.SecretReminderMigration // just a job id ); - - await queueService.queue(QueueName.SecretReminderMigration, QueueJobs.SecretReminderMigration, undefined, { - delay: 5000, - jobId: QueueName.SecretReminderMigration, - repeat: { pattern: "0 */1 * * *", utc: true } - }); }; queueService.listen(QueueName.DailyReminders, "failed", (_, err) => { From c368178cb166e10040016f6483eeea284a735834 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 26 Jul 2025 03:00:44 +0800 Subject: [PATCH 48/79] feat: secrets detection in secret manager --- ...171821_add-secret-detection-ignore-keys.ts | 19 +++ backend/src/db/schemas/projects.ts | 3 +- .../secret-scanning-v2-fns.ts | 69 +++++++- backend/src/lib/api-docs/constants.ts | 3 +- backend/src/lib/config/env.ts | 12 ++ backend/src/server/routes/index.ts | 1 + backend/src/server/routes/sanitizedSchemas.ts | 3 +- backend/src/server/routes/v1/admin-router.ts | 6 +- .../src/server/routes/v1/project-router.ts | 6 +- .../src/services/project/project-service.ts | 11 +- backend/src/services/project/project-types.ts | 1 + .../secret-v2-bridge-service.ts | 45 ++++++ frontend/src/hooks/api/admin/types.ts | 1 + frontend/src/hooks/api/workspace/queries.tsx | 6 +- frontend/src/hooks/api/workspace/types.ts | 2 + .../ProjectGeneralTab/ProjectGeneralTab.tsx | 6 + .../SecretDetectionIgnoreKeysSection.tsx | 147 ++++++++++++++++++ 17 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts create mode 100644 frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx diff --git a/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts new file mode 100644 index 000000000..dfdf5ecfc --- /dev/null +++ b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys"))) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.specificType("secretDetectionIgnoreKeys", "text[]"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys")) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("secretDetectionIgnoreKeys"); + }); + } +} diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 059565a94..fd8b273f2 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -30,7 +30,8 @@ export const ProjectsSchema = z.object({ hasDeleteProtection: z.boolean().default(false).nullable().optional(), secretSharing: z.boolean().default(true), showSnapshotsLegacy: z.boolean().default(false), - defaultProduct: z.string().nullable().optional() + defaultProduct: z.string().nullable().optional(), + secretDetectionIgnoreKeys: z.string().array().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts index 9489f4658..917f992d3 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts @@ -1,11 +1,20 @@ import { AxiosError } from "axios"; import { exec } from "child_process"; +import { join } from "path"; +import picomatch from "picomatch"; import RE2 from "re2"; -import { readFindingsFile } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns"; +import { + createTempFolder, + deleteTempFolder, + readFindingsFile, + writeTextToFile +} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns"; import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; import { BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/github"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; import { titleCaseToCamelCase } from "@app/lib/fn"; import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums"; @@ -46,6 +55,19 @@ export function scanDirectory(inputPath: string, outputPath: string, configPath? }); } +export function scanFile(inputPath: string): Promise { + return new Promise((resolve, reject) => { + const command = `infisical scan --exit-code=77 --source "${inputPath}" --no-git`; + exec(command, (error) => { + if (error && error.code === 77) { + reject(error); + } else { + resolve(); + } + }); + }); +} + export const scanGitRepositoryAndGetFindings = async ( scanPath: string, findingsPath: string, @@ -140,3 +162,48 @@ export const parseScanErrorMessage = (err: unknown): string => { ? errorMessage : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; }; + +export const scanSecretPolicyViolations = async ( + secretPath: string, + secrets: { secretKey: string; secretValue: string }[], + ignoreKeys: string[] +) => { + const appCfg = getConfig(); + + if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) { + return; + } + const paramFolderSecretDetectionPaths = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.map((el) => el.secretPath) ?? []; + const isPathMatched = paramFolderSecretDetectionPaths.some((pattern) => + picomatch.isMatch(secretPath, pattern, { strictSlashes: false }) + ); + + if (!isPathMatched) { + return; + } + + const tempFolder = await createTempFolder(); + try { + let iter = 0; + for await (const secret of secrets) { + if (ignoreKeys.includes(secret.secretKey)) { + // eslint-disable-next-line no-continue + continue; + } + + iter += 1; + const secretFilePath = join(tempFolder, `${iter}.txt`); + await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`); + try { + await scanFile(secretFilePath); + } catch (error) { + throw new BadRequestError({ + message: `Secret value detected in ${secret.secretKey}. Please add this instead to the designated secrets path in the project.`, + name: "SecretPolicyViolation" + }); + } + } + } finally { + await deleteTempFolder(tempFolder); + } +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b6c00985a..f42431195 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -704,7 +704,8 @@ export const PROJECTS = { hasDeleteProtection: "Enable or disable delete protection for the project.", secretSharing: "Enable or disable secret sharing for the project.", showSnapshotsLegacy: "Enable or disable legacy snapshots for the project.", - defaultProduct: "The default product in which the project will open" + defaultProduct: "The default product in which the project will open", + secretDetectionIgnoreKeys: "The list of secret keys to ignore for secret detection." }, GET_KEY: { workspaceId: "The ID of the project to get the key from." diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 986963e47..af47a6537 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -204,6 +204,18 @@ const envSchema = z WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()), ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"), + // Special Detection Feature + PARAMS_FOLDER_SECRET_DETECTION_PATHS: zpStr( + z + .string() + .optional() + .transform((val) => { + if (!val) return undefined; + return JSON.parse(val) as { secretPath: string }[]; + }) + ), + PARAMS_FOLDER_SECRET_DETECTION_ENABLED: zodStrBool.default("false"), + // HSM HSM_LIB_PATH: zpStr(z.string().optional()), HSM_PIN: zpStr(z.string().optional()), diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index ea4cf9676..01f58494b 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1231,6 +1231,7 @@ export const registerRoutes = async ( const secretV2BridgeService = secretV2BridgeServiceFactory({ folderDAL, + projectDAL, secretVersionDAL: secretVersionV2BridgeDAL, folderCommitService, secretQueueService, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index beef663b9..1564b4bcb 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -264,7 +264,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ auditLogsRetentionDays: true, hasDeleteProtection: true, secretSharing: true, - showSnapshotsLegacy: true + showSnapshotsLegacy: true, + secretDetectionIgnoreKeys: true }); export const SanitizedTagSchema = SecretTagsSchema.pick({ diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 6cc50dc5c..57aa42fae 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -52,7 +52,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { defaultAuthOrgAuthEnforced: z.boolean().nullish(), defaultAuthOrgAuthMethod: z.string().nullish(), isSecretScanningDisabled: z.boolean(), - kubernetesAutoFetchServiceAccountToken: z.boolean() + kubernetesAutoFetchServiceAccountToken: z.boolean(), + paramsFolderSecretDetectionEnabled: z.boolean() }) }) } @@ -67,7 +68,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { fipsEnabled: crypto.isFipsModeEnabled(), isMigrationModeOn: serverEnvs.MAINTENANCE_MODE, isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING, - kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN + kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN, + paramsFolderSecretDetectionEnabled: serverEnvs.PARAMS_FOLDER_SECRET_DETECTION_ENABLED } }; } diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 05aade960..5992b1a33 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -369,7 +369,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .describe(PROJECTS.UPDATE.slug), secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing), showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy), - defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct) + defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct), + secretDetectionIgnoreKeys: z.array(z.string()).optional().describe(PROJECTS.UPDATE.secretDetectionIgnoreKeys) }), response: { 200: z.object({ @@ -392,7 +393,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { hasDeleteProtection: req.body.hasDeleteProtection, slug: req.body.slug, secretSharing: req.body.secretSharing, - showSnapshotsLegacy: req.body.showSnapshotsLegacy + showSnapshotsLegacy: req.body.showSnapshotsLegacy, + secretDetectionIgnoreKeys: req.body.secretDetectionIgnoreKeys }, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 27925c4b9..a166d40c3 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -645,7 +645,7 @@ export const projectServiceFactory = ({ const updateProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, update, filter }: TUpdateProjectDTO) => { const project = await projectDAL.findProjectByFilter(filter); - const { permission } = await permissionService.getProjectPermission({ + const { permission, hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -667,6 +667,12 @@ export const projectServiceFactory = ({ } } + if (update.secretDetectionIgnoreKeys && !hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ + message: "Only admins can update secret detection ignore keys" + }); + } + const updatedProject = await projectDAL.updateById(project.id, { name: update.name, description: update.description, @@ -676,7 +682,8 @@ export const projectServiceFactory = ({ slug: update.slug, secretSharing: update.secretSharing, defaultProduct: update.defaultProduct, - showSnapshotsLegacy: update.showSnapshotsLegacy + showSnapshotsLegacy: update.showSnapshotsLegacy, + secretDetectionIgnoreKeys: update.secretDetectionIgnoreKeys }); return updatedProject; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index b8c37a858..82f9a0b8f 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -96,6 +96,7 @@ export type TUpdateProjectDTO = { slug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; + secretDetectionIgnoreKeys?: string[]; }; } & Omit; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 2fa0ffe9b..f4f815fbd 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -25,6 +25,7 @@ import { import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; +import { scanSecretPolicyViolations } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -38,6 +39,7 @@ import { ActorType } from "../auth/auth-type"; import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; @@ -88,6 +90,7 @@ import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal"; type TSecretV2BridgeServiceFactoryDep = { secretDAL: TSecretV2BridgeDALFactory; + projectDAL: Pick; secretVersionDAL: TSecretVersionV2DALFactory; kmsService: Pick; secretVersionTagDAL: Pick; @@ -126,6 +129,7 @@ export type TSecretV2BridgeServiceFactory = ReturnType ({ secretKey: el, secretPath, environment })) @@ -506,6 +522,20 @@ export const secretV2BridgeServiceFactory = ({ const { secretName, secretValue } = inputSecret; + if (secretValue) { + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + secretPath, + [ + { + secretKey: inputSecret.newSecretName || secretName, + secretValue + } + ], + project.secretDetectionIgnoreKeys || [] + ); + } + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId @@ -1585,6 +1615,9 @@ export const secretV2BridgeServiceFactory = ({ if (secrets.length) throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` }); + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations(secretPath, inputSecrets, project.secretDetectionIgnoreKeys || []); + // get all tags const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : []; @@ -1925,6 +1958,18 @@ export const secretV2BridgeServiceFactory = ({ }); await $validateSecretReferences(projectId, permission, secretReferences, tx); + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + secretPath, + secretsToUpdate + .filter((el) => el.secretValue) + .map((el) => ({ + secretKey: el.newSecretName || el.secretKey, + secretValue: el.secretValue as string + })), + project.secretDetectionIgnoreKeys || [] + ); + const bulkUpdatedSecrets = await fnSecretBulkUpdate({ folderId, orgId: actorOrgId, diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 6685e5b77..51ff5a908 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -51,6 +51,7 @@ export type TServerConfig = { invalidatingCache: boolean; fipsEnabled: boolean; envOverrides?: Record; + paramsFolderSecretDetectionEnabled: boolean; }; export type TUpdateServerConfigDTO = { diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 6408d0e22..e4e9d2dd1 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -281,7 +281,8 @@ export const useUpdateProject = () => { newProjectDescription, newSlug, secretSharing, - showSnapshotsLegacy + showSnapshotsLegacy, + secretDetectionIgnoreKeys }) => { const { data } = await apiRequest.patch<{ workspace: Workspace }>( `/api/v1/workspace/${projectID}`, @@ -290,7 +291,8 @@ export const useUpdateProject = () => { description: newProjectDescription, slug: newSlug, secretSharing, - showSnapshotsLegacy + showSnapshotsLegacy, + secretDetectionIgnoreKeys } ); return data.workspace; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 7d6f68821..e96b7099e 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -40,6 +40,7 @@ export type Workspace = { hasDeleteProtection: boolean; secretSharing: boolean; showSnapshotsLegacy: boolean; + secretDetectionIgnoreKeys: string[]; }; export type WorkspaceEnv = { @@ -81,6 +82,7 @@ export type UpdateProjectDTO = { newSlug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; + secretDetectionIgnoreKeys?: string[]; }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx index 5f817a46a..022951dff 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -1,12 +1,17 @@ +import { useServerConfig } from "@app/context"; + import { AutoCapitalizationSection } from "../AutoCapitalizationSection"; import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection"; import { EnvironmentSection } from "../EnvironmentSection"; import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection"; +import { SecretDetectionIgnoreKeysSection } from "../SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection"; import { SecretSharingSection } from "../SecretSharingSection"; import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection"; import { SecretTagsSection } from "../SecretTagsSection"; export const SecretSettingsTab = () => { + const { config } = useServerConfig(); + return (
@@ -15,6 +20,7 @@ export const SecretSettingsTab = () => { + {config.paramsFolderSecretDetectionEnabled && }
); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx new file mode 100644 index 000000000..bc3f04b98 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx @@ -0,0 +1,147 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, IconButton, Input } from "@app/components/v2"; +import { useProjectPermission, useWorkspace } from "@app/context"; +import { useUpdateProject } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; + +const formSchema = z.object({ + ignoreKeys: z + .object({ + key: z.string().trim().min(1, "Secret key name is required") + }) + .array() + .default([]) +}); + +type TForm = z.infer; + +export const SecretDetectionIgnoreKeysSection = () => { + const { currentWorkspace } = useWorkspace(); + const { membership } = useProjectPermission(); + const { mutateAsync: updateProject } = useUpdateProject(); + + const { + control, + formState: { isSubmitting, isDirty }, + handleSubmit, + reset + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + ignoreKeys: [] + } + }); + + const ignoreKeysFormFields = useFieldArray({ + control, + name: "ignoreKeys" + }); + + useEffect(() => { + const existingIgnoreKeys = currentWorkspace?.secretDetectionIgnoreKeys || []; + reset({ + ignoreKeys: + existingIgnoreKeys.length > 0 ? existingIgnoreKeys.map((key) => ({ key })) : [{ key: "" }] // Show one empty field by default + }); + }, [currentWorkspace?.secretDetectionIgnoreKeys, reset]); + + const handleIgnoreKeysSubmit = async ({ ignoreKeys }: TForm) => { + try { + await updateProject({ + projectID: currentWorkspace.id, + secretDetectionIgnoreKeys: ignoreKeys.map((item) => item.key) + }); + + createNotification({ + text: "Successfully updated secret detection ignore keys", + type: "success" + }); + } catch { + createNotification({ + text: "Failed updating secret detection ignore keys", + type: "error" + }); + } + }; + + const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + + if (!currentWorkspace) return null; + + return ( +
+
+

Secret Detection Ignore Keys

+
+

+ Define secret keys that should be ignored when scanning parameter folders for misplaced + secrets. These keys will not trigger policy violation alerts even if they contain sensitive + data. +

+ + +
+

Ignored Secret Keys

+
+ {ignoreKeysFormFields.fields.map(({ id: ignoreKeyFieldId }, i) => ( +
+
+ {i === 0 && Secret Key Name} + ( + + + + )} + /> +
+ ignoreKeysFormFields.remove(i)} + isDisabled={!isAdmin} + > + + +
+ ))} +
+ +
+
+
+ + + +
+ ); +}; From 253c46f21d36e649dec638b843ae376eee0baaa7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 25 Jul 2025 23:09:23 +0400 Subject: [PATCH 49/79] fips improvements --- Dockerfile.fips.standalone-infisical | 8 ++++++-- backend/Dockerfile.dev.fips | 6 +++++- backend/src/db/migrations/utils/env-config.ts | 2 +- backend/src/lib/crypto/cryptography/crypto.ts | 16 ++++++++-------- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index d2b2a2d87..dec41a36d 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -145,7 +145,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ && cd openssl-3.1.2 \ && ./Configure enable-fips \ && make \ - && make install_fips + && make install_fips \ + && cd / \ + && rm -rf /openssl-build \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ @@ -186,7 +190,7 @@ ENV NODE_ENV production ENV STANDALONE_BUILD true ENV STANDALONE_MODE true ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ -ENV NODE_OPTIONS="--max-old-space-size=1024" +ENV NODE_OPTIONS="--max-old-space-size=8192" # FIPS mode of operation: ENV OPENSSL_CONF=/backend/nodejs.fips.cnf diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips index 977362e03..b954ccd50 100644 --- a/backend/Dockerfile.dev.fips +++ b/backend/Dockerfile.dev.fips @@ -59,7 +59,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ && cd openssl-3.1.2 \ && ./Configure enable-fips \ && make \ - && make install_fips + && make install_fips \ + && cd / \ + && rm -rf /openssl-build \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # ? App setup diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts index debaea03f..de32f4db9 100644 --- a/backend/src/db/migrations/utils/env-config.ts +++ b/backend/src/db/migrations/utils/env-config.ts @@ -53,7 +53,7 @@ export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory let envCfg = Object.freeze(parsedEnv.data); - const fipsEnabled = await crypto.initialize(superAdminDAL); + const fipsEnabled = await crypto.initialize(superAdminDAL, envCfg); // Fix for 128-bit entropy encryption key expansion issue: // In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY. diff --git a/backend/src/lib/crypto/cryptography/crypto.ts b/backend/src/lib/crypto/cryptography/crypto.ts index 967e7e007..b8fc45645 100644 --- a/backend/src/lib/crypto/cryptography/crypto.ts +++ b/backend/src/lib/crypto/cryptography/crypto.ts @@ -14,7 +14,7 @@ import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service"; import { isBase64 } from "../../base64"; -import { getConfig } from "../../config/env"; +import { getConfig, TEnvConfig } from "../../config/env"; import { CryptographyError } from "../../errors"; import { logger } from "../../logger"; import { asymmetricFipsValidated } from "./asymmetric-fips"; @@ -106,12 +106,12 @@ const cryptographyFactory = () => { } }; - const $setFipsModeEnabled = (enabled: boolean) => { + const $setFipsModeEnabled = (enabled: boolean, envCfg?: Pick) => { // If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key. if (enabled) { crypto.setFips(true); - const appCfg = getConfig(); + const appCfg = envCfg || getConfig(); if (appCfg.ENCRYPTION_KEY) { // we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key @@ -141,14 +141,14 @@ const cryptographyFactory = () => { $isInitialized = true; }; - const initialize = async (superAdminDAL: TSuperAdminDALFactory) => { + const initialize = async (superAdminDAL: TSuperAdminDALFactory, envCfg?: Pick) => { if ($isInitialized) { return isFipsModeEnabled(); } if (process.env.FIPS_ENABLED !== "true") { logger.info("Cryptography module initialized in normal operation mode."); - $setFipsModeEnabled(false); + $setFipsModeEnabled(false, envCfg); return false; } @@ -158,11 +158,11 @@ const cryptographyFactory = () => { if (serverCfg) { if (serverCfg.fipsEnabled) { logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled."); - $setFipsModeEnabled(true); + $setFipsModeEnabled(true, envCfg); return true; } logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS."); - $setFipsModeEnabled(false); + $setFipsModeEnabled(false, envCfg); return false; } @@ -171,7 +171,7 @@ const cryptographyFactory = () => { // TODO(daniel): check if it's an enterprise deployment // if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true. - $setFipsModeEnabled(true); + $setFipsModeEnabled(true, envCfg); return true; }; From d4f030110496036c4a0d0d3e111f79efb02bdbd2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 25 Jul 2025 23:13:26 +0400 Subject: [PATCH 50/79] Update Dockerfile.fips.standalone-infisical --- Dockerfile.fips.standalone-infisical | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index dec41a36d..b95794832 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -190,12 +190,11 @@ ENV NODE_ENV production ENV STANDALONE_BUILD true ENV STANDALONE_MODE true ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ -ENV NODE_OPTIONS="--max-old-space-size=8192" +ENV NODE_OPTIONS="--max-old-space-size=8192 --force-fips" # FIPS mode of operation: ENV OPENSSL_CONF=/backend/nodejs.fips.cnf ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules -ENV NODE_OPTIONS=--force-fips ENV FIPS_ENABLED=true From 7fdee073d832871e275e30e9e9d77bc6ccc7d1e4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 26 Jul 2025 03:16:39 +0800 Subject: [PATCH 51/79] misc: add secret checker in change policy branch --- .../secret-approval-request-service.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 2be0361e0..f0c719802 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -69,6 +69,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; +import { scanSecretPolicyViolations } from "../secret-scanning-v2/secret-scanning-v2-fns"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; import { sendApprovalEmailsFn } from "./secret-approval-request-fns"; @@ -1407,6 +1408,19 @@ export const secretApprovalRequestServiceFactory = ({ projectId }); + const project = await projectDAL.findById(projectId); + await scanSecretPolicyViolations( + secretPath, + [ + ...(data[SecretOperations.Create] || []), + ...(data[SecretOperations.Update] || []).filter((el) => el.secretValue) + ].map((el) => ({ + secretKey: el.secretKey, + secretValue: el.secretValue as string + })), + project.secretDetectionIgnoreKeys || [] + ); + // for created secret approval change const createdSecrets = data[SecretOperations.Create]; if (createdSecrets && createdSecrets?.length) { From 585cb1b30c86d6c01d3b9860080324f2a374d9ec Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sat, 26 Jul 2025 03:26:24 +0800 Subject: [PATCH 52/79] misc: used promise all --- .../secret-scanning-v2-fns.ts | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts index 917f992d3..99aa5b91f 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts @@ -14,6 +14,7 @@ import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-qu import { BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/bitbucket"; import { GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/github"; import { getConfig } from "@app/lib/config/env"; +import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; import { titleCaseToCamelCase } from "@app/lib/fn"; @@ -184,25 +185,23 @@ export const scanSecretPolicyViolations = async ( const tempFolder = await createTempFolder(); try { - let iter = 0; - for await (const secret of secrets) { - if (ignoreKeys.includes(secret.secretKey)) { - // eslint-disable-next-line no-continue - continue; - } + const scanPromises = secrets + .filter((secret) => !ignoreKeys.includes(secret.secretKey)) + .map(async (secret) => { + const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`); + await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`); - iter += 1; - const secretFilePath = join(tempFolder, `${iter}.txt`); - await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`); - try { - await scanFile(secretFilePath); - } catch (error) { - throw new BadRequestError({ - message: `Secret value detected in ${secret.secretKey}. Please add this instead to the designated secrets path in the project.`, - name: "SecretPolicyViolation" - }); - } - } + try { + await scanFile(secretFilePath); + } catch (error) { + throw new BadRequestError({ + message: `Secret value detected in ${secret.secretKey}. Please add this instead to the designated secrets path in the project.`, + name: "SecretPolicyViolation" + }); + } + }); + + await Promise.all(scanPromises); } finally { await deleteTempFolder(tempFolder); } From e6588b5d0e612fbdf0c7d0d6cbdbe8cf3bcb66ed Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 17:00:11 -0300 Subject: [PATCH 53/79] Set correct environmentName on listApprovalRequests --- .../access-approval-request-service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index e9b311905..dcbe717da 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -327,6 +327,15 @@ export const accessApprovalRequestServiceFactory = ({ requests = requests.filter((request) => request.environment === envSlug); } + requests = requests.map((request) => { + const permissionEnvironment = $getEnvironmentFromPermissions(request.permissions); + + if (permissionEnvironment) { + request.environmentName = permissionEnvironment; + } + return request; + }); + return { requests }; }; From 3400a8f911ec12b67338e9928d24b376fb7865a3 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 17:24:15 -0300 Subject: [PATCH 54/79] Small UI fix for environments label --- .../ApprovalPolicyList/components/AccessPolicyModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index e160a88f0..b254b8ac7 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -490,7 +490,7 @@ const Form = ({ name="environments" render={({ field: { value, onChange }, fieldState: { error } }) => ( option.slug} getOptionLabel={(option) => option.name} From 0b7b32bdc367a4c712d580bcacf38fe6fa12af59 Mon Sep 17 00:00:00 2001 From: x032205 Date: Fri, 25 Jul 2025 16:55:21 -0400 Subject: [PATCH 55/79] Add proper URI component encoding + hostname check --- backend/src/lib/validator/validate-url.ts | 5 ++++ .../github/github-connection-fns.ts | 2 +- .../secret-sync/github/github-sync-fns.ts | 24 +++++++++---------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/backend/src/lib/validator/validate-url.ts b/backend/src/lib/validator/validate-url.ts index 8f195e0b5..a4c07b37d 100644 --- a/backend/src/lib/validator/validate-url.ts +++ b/backend/src/lib/validator/validate-url.ts @@ -14,6 +14,11 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => { if (appCfg.isDevelopmentMode) return; const validUrl = new URL(url); + + if (validUrl.username || validUrl.password) { + throw new BadRequestError({ message: "URLs with user credentials (e.g., user:pass@) are not allowed" }); + } + const inputHostIps: string[] = []; if (isIPv4(validUrl.hostname)) { inputHostIps.push(validUrl.hostname); diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index b38b9406b..57d01be29 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -241,7 +241,7 @@ export const getGitHubEnvironments = async ( return await makePaginatedGitHubRequest( appConnection, gatewayService, - `/repos/${owner}/${repo}/environments`, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/environments`, (data) => data.environments ); } catch (error) { diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index 022490da6..b37d5e90e 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -26,16 +26,16 @@ const getEncryptedSecrets = async ( let path: string; switch (destinationConfig.scope) { case GitHubSyncScope.Organization: { - path = `/orgs/${destinationConfig.org}/actions/secrets`; + path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets`; break; } case GitHubSyncScope.Repository: { - path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets`; + path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets`; break; } case GitHubSyncScope.RepositoryEnvironment: default: { - path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets`; + path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets`; break; } } @@ -58,16 +58,16 @@ const getPublicKey = async ( let path: string; switch (destinationConfig.scope) { case GitHubSyncScope.Organization: { - path = `/orgs/${destinationConfig.org}/actions/secrets/public-key`; + path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/public-key`; break; } case GitHubSyncScope.Repository: { - path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/public-key`; + path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/public-key`; break; } case GitHubSyncScope.RepositoryEnvironment: default: { - path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/public-key`; + path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/public-key`; break; } } @@ -96,16 +96,16 @@ const deleteSecret = async ( let path: string; switch (destinationConfig.scope) { case GitHubSyncScope.Organization: { - path = `/orgs/${destinationConfig.org}/actions/secrets/${encryptedSecret.name}`; + path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/${encodeURIComponent(encryptedSecret.name)}`; break; } case GitHubSyncScope.Repository: { - path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/${encryptedSecret.name}`; + path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/${encodeURIComponent(encryptedSecret.name)}`; break; } case GitHubSyncScope.RepositoryEnvironment: default: { - path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/${encryptedSecret.name}`; + path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/${encodeURIComponent(encryptedSecret.name)}`; break; } } @@ -135,7 +135,7 @@ const putSecret = async ( switch (destinationConfig.scope) { case GitHubSyncScope.Organization: { const { visibility, selectedRepositoryIds } = destinationConfig; - path = `/orgs/${destinationConfig.org}/actions/secrets/${payload.secret_name}`; + path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/${encodeURIComponent(payload.secret_name)}`; body = { ...payload, visibility, @@ -146,12 +146,12 @@ const putSecret = async ( break; } case GitHubSyncScope.Repository: { - path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/actions/secrets/${payload.secret_name}`; + path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/${encodeURIComponent(payload.secret_name)}`; break; } case GitHubSyncScope.RepositoryEnvironment: default: { - path = `/repos/${destinationConfig.owner}/${destinationConfig.repo}/environments/${destinationConfig.env}/secrets/${payload.secret_name}`; + path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/${encodeURIComponent(payload.secret_name)}`; break; } } From 0adf2c830dc81137d3be3ab89f00e6ae2faa7582 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 20:47:17 -0300 Subject: [PATCH 56/79] Fix azure client secrets OAuth URL to use graph instead of vault --- .../app-connections/azure-client-secrets.mdx | 18 ++---------------- .../AzureClientSecretsConnectionForm.tsx | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/docs/integrations/app-connections/azure-client-secrets.mdx b/docs/integrations/app-connections/azure-client-secrets.mdx index fc4194c5e..1fb1c753f 100644 --- a/docs/integrations/app-connections/azure-client-secrets.mdx +++ b/docs/integrations/app-connections/azure-client-secrets.mdx @@ -43,12 +43,6 @@ Infisical currently only supports one method for connecting to Azure, which is O - `Application.ReadWrite.All` (Delegated) - `Directory.ReadWrite.All` (Delegated) - `User.Read` (Delegated) - - Azure App Configuration - - `KeyValue.Delete` (Delegated) - - `KeyValue.Read` (Delegated) - - `KeyValue.Write` (Delegated) - - Access Key Vault - - `user_impersonation` (Delegated) ![Azure client secrets](/images/integrations/azure-client-secrets/app-api-permissions.png) @@ -63,8 +57,8 @@ Infisical currently only supports one method for connecting to Azure, which is O Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET`: The **Client Secret** of your Azure application. Once added, restart your Infisical instance and use the Azure Client Secrets connection. @@ -91,14 +85,6 @@ Infisical currently only supports one method for connecting to Azure, which is O - `Directory.ReadWrite.All` (Delegated) - `User.Read` (Delegated) - **Azure App Configuration** - - `KeyValue.Delete` (Delegated) - - `KeyValue.Read` (Delegated) - - `KeyValue.Write` (Delegated) - - **Access Key Vault** - - `user_impersonation` (Delegated) - ![Azure client secrets](/images/integrations/azure-client-secrets/app-api-permissions.png) diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx index 6aa6ee63c..f6c5788f4 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AzureClientSecretsConnectionForm.tsx @@ -132,7 +132,7 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr JSON.stringify({ ...formData, connectionId: appConnection?.id }) ); window.location.assign( - `https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets` + `https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets` ); break; From 68401a799ef17c3c6737ac2be126f649016357b4 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 20:48:18 -0300 Subject: [PATCH 57/79] Fix env variables name on doc --- docs/integrations/app-connections/azure-client-secrets.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/app-connections/azure-client-secrets.mdx b/docs/integrations/app-connections/azure-client-secrets.mdx index 1fb1c753f..82382733e 100644 --- a/docs/integrations/app-connections/azure-client-secrets.mdx +++ b/docs/integrations/app-connections/azure-client-secrets.mdx @@ -57,8 +57,8 @@ Infisical currently only supports one method for connecting to Azure, which is O Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET`: The **Client Secret** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. Once added, restart your Infisical instance and use the Azure Client Secrets connection. From 484f34a25726be2eeacc2f4a0008bda50c10168c Mon Sep 17 00:00:00 2001 From: = Date: Mon, 28 Jul 2025 00:03:01 +0530 Subject: [PATCH 58/79] fix: potential fix for oracle db rotation failing --- .../sql-credentials-rotation-fns.ts | 28 +++++++++++++++---- .../secret-rotation-v2/shared/utils/index.ts | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts index 3e6e5d265..15832fe99 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -7,12 +7,13 @@ import { TRotationFactoryRevokeCredentials, TRotationFactoryRotateCredentials } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { executeWithPotentialGateway, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql"; -import { generatePassword } from "../utils"; +import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../utils"; import { TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationWithConnection @@ -32,6 +33,11 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat return redactedMessage; }; +const ORACLE_PASSWORD_REQUIREMENTS = { + ...DEFAULT_PASSWORD_REQUIREMENTS, + length: 30 +}; + export const sqlCredentialsRotationFactory: TRotationFactory< TSqlCredentialsRotationWithConnection, TSqlCredentialsRotationGeneratedCredentials @@ -43,6 +49,9 @@ export const sqlCredentialsRotationFactory: TRotationFactory< secretsMapping } = secretRotation; + const passwordRequirement = + connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS; + const executeOperation = ( operation: (client: Knex) => Promise, credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number] @@ -65,7 +74,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory< const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => { try { await executeOperation(async (client) => { - await client.raw("SELECT 1"); + await client.raw(connection.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`); }, credentials); } catch (error) { throw new Error(redactPasswords(error, [credentials])); @@ -75,11 +84,12 @@ export const sqlCredentialsRotationFactory: TRotationFactory< const issueCredentials: TRotationFactoryIssueCredentials = async ( callback ) => { + // const connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS // For SQL, since we get existing users, we change both their passwords // on issue to invalidate their existing passwords const credentialsSet = [ - { username: username1, password: generatePassword() }, - { username: username2, password: generatePassword() } + { username: username1, password: generatePassword(passwordRequirement) }, + { username: username2, password: generatePassword(passwordRequirement) } ]; try { @@ -105,7 +115,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory< credentialsToRevoke, callback ) => { - const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() })); + const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ + username, + password: generatePassword(passwordRequirement) + })); try { await executeOperation(async (client) => { @@ -128,7 +141,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory< callback ) => { // generate new password for the next active user - const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() }; + const credentials = { + username: activeIndex === 0 ? username2 : username1, + password: generatePassword(passwordRequirement) + }; try { await executeOperation(async (client) => { diff --git a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts index ef58687a1..4122abfda 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts @@ -11,7 +11,7 @@ type TPasswordRequirements = { allowedSymbols?: string; }; -const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = { +export const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = { length: 48, required: { lowercase: 1, From 8df461626539ab9d1fca130adfdc115635b77de6 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Mon, 28 Jul 2025 00:09:30 +0530 Subject: [PATCH 59/79] Update backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../shared/sql-credentials/sql-credentials-rotation-fns.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts index 15832fe99..1da1db376 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -84,7 +84,8 @@ export const sqlCredentialsRotationFactory: TRotationFactory< const issueCredentials: TRotationFactoryIssueCredentials = async ( callback ) => { - // const connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS + // For SQL, since we get existing users, we change both their passwords + // on issue to invalidate their existing passwords // For SQL, since we get existing users, we change both their passwords // on issue to invalidate their existing passwords const credentialsSet = [ From 0779091d1fcc1bb4d4035fbc118ed4ff477e326b Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 28 Jul 2025 09:14:43 -0300 Subject: [PATCH 60/79] Separate Azure OAuth env vars to different env variables for each app connection --- .env.example | 12 ++++++ backend/src/lib/config/env.ts | 40 +++++++++++++++++++ .../azure-app-configuration-connection-fns.ts | 22 +++++++--- .../azure-client-secrets-connection-fns.ts | 32 ++++++++++----- .../azure-devops/azure-devops-fns.ts | 30 +++++++++----- .../azure-key-vault-connection-fns.ts | 31 +++++++++----- .../azure-app-configuration.mdx | 4 +- .../app-connections/azure-client-secrets.mdx | 4 +- .../app-connections/azure-devops.mdx | 4 +- .../app-connections/azure-key-vault.mdx | 4 +- 10 files changed, 142 insertions(+), 41 deletions(-) diff --git a/.env.example b/.env.example index 05a888db0..dbaf1e633 100644 --- a/.env.example +++ b/.env.example @@ -126,6 +126,18 @@ INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= INF_APP_CONNECTION_AZURE_CLIENT_ID= INF_APP_CONNECTION_AZURE_CLIENT_SECRET= +INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID= +INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET= + +INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID= +INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET= + +INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID= +INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET= + +INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID= +INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET= + # datadog SHOULD_USE_DATADOG_TRACER= DATADOG_PROFILING_ENABLED= diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 986963e47..d6eeba342 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -264,6 +264,14 @@ const envSchema = z // azure app INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()), // datadog SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"), @@ -461,6 +469,38 @@ export const overwriteSchema: { { key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET", description: "The Client Secret of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID", + description: "The Application (Client) ID of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET", + description: "The Client Secret of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID", + description: "The Application (Client) ID of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET", + description: "The Client Secret of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID", + description: "The Application (Client) ID of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET", + description: "The Client Secret of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID", + description: "The Application (Client) ID of your Azure application." + }, + { + key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET", + description: "The Client Secret of your Azure application." } ] }, diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts index 937a8a84f..9fd38be9d 100644 --- a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts @@ -14,13 +14,13 @@ import { } from "./azure-app-configuration-connection-types"; export const getAzureAppConfigurationConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID } = getConfig(); return { name: "Azure App Configuration" as const, app: AppConnection.AzureAppConfiguration as const, methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth], - oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID }; }; @@ -29,9 +29,19 @@ export const validateAzureAppConfigurationConnectionCredentials = async ( ) => { const { credentials: inputCredentials, method } = config; - const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); + const { + INF_APP_CONNECTION_AZURE_CLIENT_ID, + INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID, + INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET, + SITE_URL + } = getConfig(); - if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + const azureClientId = INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; + const azureClientSecret = + INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + + if (!azureClientId || !azureClientSecret) { throw new InternalServerError({ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` }); @@ -47,8 +57,8 @@ export const validateAzureAppConfigurationConnectionCredentials = async ( grant_type: "authorization_code", code: inputCredentials.code, scope: `openid offline_access https://azconfig.io/.default`, - client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, - client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + client_id: azureClientId, + client_secret: azureClientSecret, redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` }) ); diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index a28217320..f6987bbe9 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -23,7 +23,7 @@ import { } from "./azure-client-secrets-connection-types"; export const getAzureClientSecretsConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID } = getConfig(); return { name: "Azure Client Secrets" as const, @@ -32,7 +32,7 @@ export const getAzureClientSecretsConnectionListItem = () => { AzureClientSecretsConnectionMethod.OAuth, AzureClientSecretsConnectionMethod.ClientSecret ], - oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID }; }; @@ -64,7 +64,11 @@ export const getAzureConnectionAccessToken = async ( const currentTime = Date.now(); switch (appConnection.method) { case AzureClientSecretsConnectionMethod.OAuth: - if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + const azureClientId = + appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID; + const azureClientSecret = + appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + if (!azureClientId || !azureClientSecret) { throw new BadRequestError({ message: `Azure OAuth environment variables have not been configured` }); @@ -74,8 +78,8 @@ export const getAzureConnectionAccessToken = async ( new URLSearchParams({ grant_type: "refresh_token", scope: `openid offline_access https://graph.microsoft.com/.default`, - client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, - client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + client_id: azureClientId, + client_secret: azureClientSecret, refresh_token: refreshToken }) ); @@ -142,7 +146,13 @@ export const getAzureConnectionAccessToken = async ( export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => { const { credentials: inputCredentials, method } = config; - const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); + const { + INF_APP_CONNECTION_AZURE_CLIENT_ID, + INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID, + INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET, + SITE_URL + } = getConfig(); switch (method) { case AzureClientSecretsConnectionMethod.OAuth: @@ -150,7 +160,11 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); } - if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + const azureClientId = INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; + const azureClientSecret = + INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + + if (!azureClientId || !azureClientSecret) { throw new InternalServerError({ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` }); @@ -166,8 +180,8 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA grant_type: "authorization_code", code: inputCredentials.code, scope: `openid offline_access https://graph.microsoft.com/.default`, - client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, - client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + client_id: azureClientId, + client_secret: azureClientSecret, redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` }) ); diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts index 644747353..2c0521081 100644 --- a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts +++ b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts @@ -23,7 +23,7 @@ import { } from "./azure-devops-types"; export const getAzureDevopsConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID } = getConfig(); return { name: "Azure DevOps" as const, @@ -32,7 +32,7 @@ export const getAzureDevopsConnectionListItem = () => { AzureDevOpsConnectionMethod.OAuth, AzureDevOpsConnectionMethod.AccessToken ], - oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID }; }; @@ -63,7 +63,11 @@ export const getAzureDevopsConnection = async ( switch (appConnection.method) { case AzureDevOpsConnectionMethod.OAuth: const appCfg = getConfig(); - if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + const azureClientId = + appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID; + const azureClientSecret = + appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + if (!azureClientId || !azureClientSecret) { throw new BadRequestError({ message: `Azure environment variables have not been configured` }); @@ -81,8 +85,8 @@ export const getAzureDevopsConnection = async ( new URLSearchParams({ grant_type: "refresh_token", scope: `https://app.vssps.visualstudio.com/.default`, - client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, - client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + client_id: azureClientId, + client_secret: azureClientSecret, refresh_token: refreshToken }) ); @@ -119,7 +123,13 @@ export const getAzureDevopsConnection = async ( export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => { const { credentials: inputCredentials, method } = config; - const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); + const { + INF_APP_CONNECTION_AZURE_CLIENT_ID, + INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID, + INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET, + SITE_URL + } = getConfig(); switch (method) { case AzureDevOpsConnectionMethod.OAuth: @@ -127,7 +137,9 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); } - if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + const azureClientId = INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; + const azureClientSecret = INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + if (!azureClientId || !azureClientSecret) { throw new InternalServerError({ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` }); @@ -144,8 +156,8 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev grant_type: "authorization_code", code: oauthCredentials.code, scope: `https://app.vssps.visualstudio.com/.default`, - client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, - client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + client_id: azureClientId, + client_secret: azureClientSecret, redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` }) ); diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts index 116597ec4..af8ec360c 100644 --- a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -26,7 +26,11 @@ export const getAzureConnectionAccessToken = async ( kmsService: Pick ) => { const appCfg = getConfig(); - if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + const azureClientId = + appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID; + const azureClientSecret = + appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + if (!azureClientId || !azureClientSecret) { throw new BadRequestError({ message: `Azure environment variables have not been configured` }); @@ -57,8 +61,8 @@ export const getAzureConnectionAccessToken = async ( new URLSearchParams({ grant_type: "refresh_token", scope: `openid offline_access`, - client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID, - client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + client_id: azureClientId, + client_secret: azureClientSecret, refresh_token: credentials.refreshToken }) ); @@ -92,22 +96,31 @@ export const getAzureConnectionAccessToken = async ( }; export const getAzureKeyVaultConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); return { name: "Azure Key Vault" as const, app: AppConnection.AzureKeyVault as const, methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth], - oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID }; }; export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => { const { credentials: inputCredentials, method } = config; - const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig(); + const { + INF_APP_CONNECTION_AZURE_CLIENT_ID, + INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, + INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, + SITE_URL + } = getConfig(); - if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) { + const azureClientId = INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; + const azureClientSecret = INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + + if (!azureClientId || !azureClientSecret) { throw new InternalServerError({ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` }); @@ -123,8 +136,8 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK grant_type: "authorization_code", code: inputCredentials.code, scope: `openid offline_access https://vault.azure.net/.default`, - client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID, - client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + client_id: azureClientId, + client_secret: azureClientSecret, redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` }) ); diff --git a/docs/integrations/app-connections/azure-app-configuration.mdx b/docs/integrations/app-connections/azure-app-configuration.mdx index 959a1812a..679839878 100644 --- a/docs/integrations/app-connections/azure-app-configuration.mdx +++ b/docs/integrations/app-connections/azure-app-configuration.mdx @@ -50,8 +50,8 @@ Infisical currently only supports one method for connecting to Azure, which is O Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + - `INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET`: The **Client Secret** of your Azure application. Once added, restart your Infisical instance and use the Azure App Configuration connection. diff --git a/docs/integrations/app-connections/azure-client-secrets.mdx b/docs/integrations/app-connections/azure-client-secrets.mdx index 82382733e..1fb1c753f 100644 --- a/docs/integrations/app-connections/azure-client-secrets.mdx +++ b/docs/integrations/app-connections/azure-client-secrets.mdx @@ -57,8 +57,8 @@ Infisical currently only supports one method for connecting to Azure, which is O Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET`: The **Client Secret** of your Azure application. Once added, restart your Infisical instance and use the Azure Client Secrets connection. diff --git a/docs/integrations/app-connections/azure-devops.mdx b/docs/integrations/app-connections/azure-devops.mdx index 8fcc25427..6a9e71430 100644 --- a/docs/integrations/app-connections/azure-devops.mdx +++ b/docs/integrations/app-connections/azure-devops.mdx @@ -56,8 +56,8 @@ Infisical currently supports two methods for connecting to Azure DevOps, which a Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + - `INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET`: The **Client Secret** of your Azure application. Once added, restart your Infisical instance and use the Azure Client Secrets connection. diff --git a/docs/integrations/app-connections/azure-key-vault.mdx b/docs/integrations/app-connections/azure-key-vault.mdx index f73dab834..22cdcf637 100644 --- a/docs/integrations/app-connections/azure-key-vault.mdx +++ b/docs/integrations/app-connections/azure-key-vault.mdx @@ -49,8 +49,8 @@ Infisical currently only supports one method for connecting to Azure, which is O Back in your Infisical instance, add two new environment variables for the credentials of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_ID`: The **Application (Client) ID** of your Azure application. - - `INF_APP_CONNECTION_AZURE_CLIENT_SECRET`: The **Client Secret** of your Azure application. + - `INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID`: The **Application (Client) ID** of your Azure application. + - `INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET`: The **Client Secret** of your Azure application. Once added, restart your Infisical instance and use the Azure Key Vault connection. From cd4b9cd03a23a433d7357949f6f4d66aea751ebf Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 28 Jul 2025 09:30:37 -0300 Subject: [PATCH 61/79] Improve azure client secrets env var name --- .env.example | 4 ++-- backend/src/lib/config/env.ts | 8 ++++---- .../azure-client-secrets-connection-fns.ts | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index dbaf1e633..847b5e05c 100644 --- a/.env.example +++ b/.env.example @@ -132,8 +132,8 @@ INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET= INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID= INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET= -INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID= -INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET= +INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID= +INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET= INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID= INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET= diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index d6eeba342..551ff41eb 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -268,8 +268,8 @@ const envSchema = z INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET: zpStr(z.string().optional()), - INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID: zpStr(z.string().optional()), - INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID: zpStr(z.string().optional()), + INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()), @@ -487,11 +487,11 @@ export const overwriteSchema: { description: "The Client Secret of your Azure application." }, { - key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID", + key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID", description: "The Application (Client) ID of your Azure application." }, { - key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET", + key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET", description: "The Client Secret of your Azure application." }, { diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index f6987bbe9..3dc2f12d1 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -23,7 +23,7 @@ import { } from "./azure-client-secrets-connection-types"; export const getAzureClientSecretsConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID } = getConfig(); return { name: "Azure Client Secrets" as const, @@ -32,7 +32,7 @@ export const getAzureClientSecretsConnectionListItem = () => { AzureClientSecretsConnectionMethod.OAuth, AzureClientSecretsConnectionMethod.ClientSecret ], - oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID }; }; @@ -65,9 +65,9 @@ export const getAzureConnectionAccessToken = async ( switch (appConnection.method) { case AzureClientSecretsConnectionMethod.OAuth: const azureClientId = - appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID; + appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID; const azureClientSecret = - appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET; if (!azureClientId || !azureClientSecret) { throw new BadRequestError({ message: `Azure OAuth environment variables have not been configured` @@ -149,8 +149,8 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, - INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID, - INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET, + INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID, + INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET, SITE_URL } = getConfig(); @@ -160,9 +160,9 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); } - const azureClientId = INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; + const azureClientId = INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; const azureClientSecret = - INF_APP_CONNECTION_AZURE_CLIENT_SECRET_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; + INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; if (!azureClientId || !azureClientSecret) { throw new InternalServerError({ From 27da14df9dca84872d243cbfa5a5195ad603e787 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 28 Jul 2025 16:40:20 +0400 Subject: [PATCH 62/79] Fix CVE's --- backend/package-lock.json | 31 +-- backend/package.json | 3 +- frontend/package-lock.json | 40 ++-- frontend/package.json | 2 +- package-lock.json | 453 +++++++++++++++++++++++++++++++++++++ package.json | 1 + 6 files changed, 491 insertions(+), 39 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index a5c106540..cb9efa148 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "backend", "version": "1.0.0", + "hasInstallScript": true, "license": "ISC", "dependencies": { "@aws-sdk/client-elasticache": "^3.637.0", @@ -61,7 +62,7 @@ "ajv": "^8.12.0", "argon2": "^0.31.2", "aws-sdk": "^2.1553.0", - "axios": "^1.6.7", + "axios": "^1.11.0", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", "botbuilder": "^4.23.2", @@ -13699,14 +13700,16 @@ } }, "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", - "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, "engines": { @@ -15230,13 +15233,13 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -18761,13 +18764,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { diff --git a/backend/package.json b/backend/package.json index bd355dd22..dcd36ee0b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,7 @@ "outputPath": "binary" }, "scripts": { + "preinstall": "npm-force-resolutions", "binary:build": "npm run binary:clean && npm run build:frontend && npm run build && npm run binary:babel-frontend && npm run binary:babel-backend && npm run binary:rename-imports", "binary:package": "pkg --no-bytecode --public-packages \"*\" --public --target host .", "binary:babel-backend": " babel ./dist -d ./dist", @@ -181,7 +182,7 @@ "ajv": "^8.12.0", "argon2": "^0.31.2", "aws-sdk": "^2.1553.0", - "axios": "^1.6.7", + "axios": "^1.11.0", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", "botbuilder": "^4.23.2", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index edbf5673f..3c73d9fe3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -55,7 +55,7 @@ "@ucast/mongo2js": "^1.3.4", "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", - "axios": "^1.7.9", + "axios": "^1.11.0", "classnames": "^2.5.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", @@ -5282,13 +5282,13 @@ } }, "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -5700,7 +5700,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6665,7 +6664,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -6819,7 +6817,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6829,7 +6826,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6867,7 +6863,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -6877,15 +6872,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -7855,13 +7850,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -7992,7 +7989,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -8139,7 +8135,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8215,7 +8210,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8228,7 +8222,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -9545,7 +9538,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/frontend/package.json b/frontend/package.json index 9a2cc2ba4..1bc55c1c7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,7 +59,7 @@ "@ucast/mongo2js": "^1.3.4", "@xyflow/react": "^12.4.4", "argon2-browser": "^1.18.0", - "axios": "^1.7.9", + "axios": "^1.11.0", "classnames": "^2.5.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", diff --git a/package-lock.json b/package-lock.json index 4d72220af..5a6260a40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "license": "ISC", "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", + "axios": "^1.11.0", "secrets.js-grempe": "^2.0.0" }, "devDependencies": { @@ -564,6 +565,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -580,6 +598,19 @@ "concat-map": "0.0.1" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -624,6 +655,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -670,6 +713,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -682,6 +734,65 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -921,12 +1032,94 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -975,6 +1168,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -991,6 +1196,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/husky": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", @@ -1173,6 +1417,36 @@ "loose-envify": "cli.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1305,6 +1579,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1894,6 +2174,21 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1910,6 +2205,15 @@ "concat-map": "0.0.1" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1941,6 +2245,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1973,6 +2285,11 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1982,6 +2299,45 @@ "esutils": "^2.0.2" } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2153,12 +2509,60 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, + "form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2191,6 +2595,11 @@ "type-fest": "^0.20.2" } }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2203,6 +2612,27 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "husky": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", @@ -2335,6 +2765,24 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2430,6 +2878,11 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index db15de3fb..71d278fa3 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", + "axios": "^1.11.0", "secrets.js-grempe": "^2.0.0" } } From 17af33372cc08b90af2851400f98e7e0cda28b07 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 28 Jul 2025 16:40:58 +0400 Subject: [PATCH 63/79] uninstall axios in root --- package-lock.json | 453 ---------------------------------------------- package.json | 1 - 2 files changed, 454 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a6260a40..4d72220af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "license": "ISC", "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", - "axios": "^1.11.0", "secrets.js-grempe": "^2.0.0" }, "devDependencies": { @@ -565,23 +564,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -598,19 +580,6 @@ "concat-map": "0.0.1" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -655,18 +624,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -713,15 +670,6 @@ "dev": true, "license": "MIT" }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -734,65 +682,6 @@ "node": ">=6.0.0" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1032,94 +921,12 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1168,18 +975,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1196,45 +991,6 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/husky": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", @@ -1417,36 +1173,6 @@ "loose-envify": "cli.js" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1579,12 +1305,6 @@ "node": ">= 0.8.0" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2174,21 +1894,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", - "requires": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2205,15 +1910,6 @@ "concat-map": "0.0.1" } }, - "call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - } - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2245,14 +1941,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2285,11 +1973,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2299,45 +1982,6 @@ "esutils": "^2.0.2" } }, - "dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "requires": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - } - }, - "es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "requires": { - "es-errors": "^1.3.0" - } - }, - "es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "requires": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - } - }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2509,60 +2153,12 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" - }, - "form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "requires": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - } - }, - "get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "requires": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - } - }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2595,11 +2191,6 @@ "type-fest": "^0.20.2" } }, - "gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2612,27 +2203,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "requires": { - "has-symbols": "^1.0.3" - } - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "requires": { - "function-bind": "^1.1.2" - } - }, "husky": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", @@ -2765,24 +2335,6 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2878,11 +2430,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 71d278fa3..db15de3fb 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ }, "dependencies": { "@radix-ui/react-radio-group": "^1.1.3", - "axios": "^1.11.0", "secrets.js-grempe": "^2.0.0" } } From 2a5593ea309e317f776782b75c55bb689f388a0b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 28 Jul 2025 16:42:21 +0400 Subject: [PATCH 64/79] update axios in oidc sink server --- sink/oidc-server/package-lock.json | 17 +++++++++-------- sink/oidc-server/package.json | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/sink/oidc-server/package-lock.json b/sink/oidc-server/package-lock.json index 2be29633b..f1732704d 100644 --- a/sink/oidc-server/package-lock.json +++ b/sink/oidc-server/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "axios": "^1.8.3", + "axios": "^1.11.0", "dotenv": "^16.4.7", "express": "^4.21.2", "form-data": "^4.0.2", @@ -105,13 +105,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -571,14 +571,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { diff --git a/sink/oidc-server/package.json b/sink/oidc-server/package.json index 514629d46..3dfa41224 100644 --- a/sink/oidc-server/package.json +++ b/sink/oidc-server/package.json @@ -11,7 +11,7 @@ "license": "ISC", "description": "", "dependencies": { - "axios": "^1.8.3", + "axios": "^1.11.0", "dotenv": "^16.4.7", "express": "^4.21.2", "form-data": "^4.0.2", From 8eebd7228f1487fac0588d7736ffe60643dc75f2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 28 Jul 2025 16:43:13 +0400 Subject: [PATCH 65/79] Update package.json --- backend/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index dcd36ee0b..01bb0c42a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,7 +25,6 @@ "outputPath": "binary" }, "scripts": { - "preinstall": "npm-force-resolutions", "binary:build": "npm run binary:clean && npm run build:frontend && npm run build && npm run binary:babel-frontend && npm run binary:babel-backend && npm run binary:rename-imports", "binary:package": "pkg --no-bytecode --public-packages \"*\" --public --target host .", "binary:babel-backend": " babel ./dist -d ./dist", From f265fa6d374f4b3736e74e1626b5286be930049c Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 28 Jul 2025 10:14:21 -0300 Subject: [PATCH 66/79] Minor improvements to azure multi env variables --- .env.example | 3 - backend/src/lib/config/env.ts | 61 ++++++++++++++----- .../azure-app-configuration-connection-fns.ts | 19 +++--- .../azure-client-secrets-connection-fns.ts | 32 +++++----- .../azure-devops/azure-devops-fns.ts | 31 +++------- .../azure-key-vault-connection-fns.ts | 35 ++++------- 6 files changed, 91 insertions(+), 90 deletions(-) diff --git a/.env.example b/.env.example index 847b5e05c..059ec124f 100644 --- a/.env.example +++ b/.env.example @@ -123,9 +123,6 @@ INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET= INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= # azure app connection -INF_APP_CONNECTION_AZURE_CLIENT_ID= -INF_APP_CONNECTION_AZURE_CLIENT_SECRET= - INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID= INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET= diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 551ff41eb..29f21ae20 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -261,15 +261,23 @@ const envSchema = z // gcp app INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()), - // azure app + // Legacy Single Multi Purpose Azure App Connection INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()), + + // Azure App Configuration App Connection INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET: zpStr(z.string().optional()), + + // Azure Key Vault App Connection INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET: zpStr(z.string().optional()), + + // Azure Client Secrets App Connection INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET: zpStr(z.string().optional()), + + // Azure DevOps App Connection INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()), @@ -349,7 +357,23 @@ const envSchema = z isHsmConfigured: Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, - SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") + SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","), + INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: + data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID, + INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: + data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID: + data.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID, + INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET: + data.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID: + data.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID, + INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET: + data.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET, + INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID: + data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID, + INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET: + data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET })); export type TEnvConfig = Readonly>; @@ -459,17 +483,9 @@ export const overwriteSchema: { } ] }, - azure: { - name: "Azure", + azureAppConfiguration: { + name: "Azure App Configuration", fields: [ - { - key: "INF_APP_CONNECTION_AZURE_CLIENT_ID", - description: "The Application (Client) ID of your Azure application." - }, - { - key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET", - description: "The Client Secret of your Azure application." - }, { key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID", description: "The Application (Client) ID of your Azure application." @@ -477,7 +493,12 @@ export const overwriteSchema: { { key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET", description: "The Client Secret of your Azure application." - }, + } + ] + }, + azureKeyVault: { + name: "Azure Key Vault", + fields: [ { key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID", description: "The Application (Client) ID of your Azure application." @@ -485,7 +506,12 @@ export const overwriteSchema: { { key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET", description: "The Client Secret of your Azure application." - }, + } + ] + }, + azureClientSecrets: { + name: "Azure Client Secrets", + fields: [ { key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID", description: "The Application (Client) ID of your Azure application." @@ -493,7 +519,12 @@ export const overwriteSchema: { { key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET", description: "The Client Secret of your Azure application." - }, + } + ] + }, + azureDevOps: { + name: "Azure DevOps", + fields: [ { key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID", description: "The Application (Client) ID of your Azure application." diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts index 9fd38be9d..114794dc5 100644 --- a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts +++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts @@ -14,13 +14,13 @@ import { } from "./azure-app-configuration-connection-types"; export const getAzureAppConfigurationConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID } = getConfig(); return { name: "Azure App Configuration" as const, app: AppConnection.AzureAppConfiguration as const, methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth], - oauthClientId: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID }; }; @@ -30,18 +30,15 @@ export const validateAzureAppConfigurationConnectionCredentials = async ( const { credentials: inputCredentials, method } = config; const { - INF_APP_CONNECTION_AZURE_CLIENT_ID, - INF_APP_CONNECTION_AZURE_CLIENT_SECRET, INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID, INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET, SITE_URL } = getConfig(); - const azureClientId = INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; - const azureClientSecret = - INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; - - if (!azureClientId || !azureClientSecret) { + if ( + !INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || + !INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET + ) { throw new InternalServerError({ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` }); @@ -57,8 +54,8 @@ export const validateAzureAppConfigurationConnectionCredentials = async ( grant_type: "authorization_code", code: inputCredentials.code, scope: `openid offline_access https://azconfig.io/.default`, - client_id: azureClientId, - client_secret: azureClientSecret, + client_id: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID, + client_secret: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET, redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` }) ); diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts index 3dc2f12d1..41dbb4392 100644 --- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts +++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts @@ -23,7 +23,7 @@ import { } from "./azure-client-secrets-connection-types"; export const getAzureClientSecretsConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID } = getConfig(); return { name: "Azure Client Secrets" as const, @@ -32,7 +32,7 @@ export const getAzureClientSecretsConnectionListItem = () => { AzureClientSecretsConnectionMethod.OAuth, AzureClientSecretsConnectionMethod.ClientSecret ], - oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID }; }; @@ -64,11 +64,10 @@ export const getAzureConnectionAccessToken = async ( const currentTime = Date.now(); switch (appConnection.method) { case AzureClientSecretsConnectionMethod.OAuth: - const azureClientId = - appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID; - const azureClientSecret = - appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET; - if (!azureClientId || !azureClientSecret) { + if ( + !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || + !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET + ) { throw new BadRequestError({ message: `Azure OAuth environment variables have not been configured` }); @@ -78,8 +77,8 @@ export const getAzureConnectionAccessToken = async ( new URLSearchParams({ grant_type: "refresh_token", scope: `openid offline_access https://graph.microsoft.com/.default`, - client_id: azureClientId, - client_secret: azureClientSecret, + client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID, + client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET, refresh_token: refreshToken }) ); @@ -147,8 +146,6 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA const { credentials: inputCredentials, method } = config; const { - INF_APP_CONNECTION_AZURE_CLIENT_ID, - INF_APP_CONNECTION_AZURE_CLIENT_SECRET, INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET, SITE_URL @@ -160,11 +157,10 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); } - const azureClientId = INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; - const azureClientSecret = - INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; - - if (!azureClientId || !azureClientSecret) { + if ( + !INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || + !INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET + ) { throw new InternalServerError({ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` }); @@ -180,8 +176,8 @@ export const validateAzureClientSecretsConnectionCredentials = async (config: TA grant_type: "authorization_code", code: inputCredentials.code, scope: `openid offline_access https://graph.microsoft.com/.default`, - client_id: azureClientId, - client_secret: azureClientSecret, + client_id: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID, + client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET, redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` }) ); diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts index 2c0521081..e9bb1f6bd 100644 --- a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts +++ b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts @@ -23,7 +23,7 @@ import { } from "./azure-devops-types"; export const getAzureDevopsConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID } = getConfig(); return { name: "Azure DevOps" as const, @@ -32,7 +32,7 @@ export const getAzureDevopsConnectionListItem = () => { AzureDevOpsConnectionMethod.OAuth, AzureDevOpsConnectionMethod.AccessToken ], - oauthClientId: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID }; }; @@ -63,11 +63,7 @@ export const getAzureDevopsConnection = async ( switch (appConnection.method) { case AzureDevOpsConnectionMethod.OAuth: const appCfg = getConfig(); - const azureClientId = - appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID; - const azureClientSecret = - appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET; - if (!azureClientId || !azureClientSecret) { + if (!appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET) { throw new BadRequestError({ message: `Azure environment variables have not been configured` }); @@ -85,8 +81,8 @@ export const getAzureDevopsConnection = async ( new URLSearchParams({ grant_type: "refresh_token", scope: `https://app.vssps.visualstudio.com/.default`, - client_id: azureClientId, - client_secret: azureClientSecret, + client_id: appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID, + client_secret: appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET, refresh_token: refreshToken }) ); @@ -123,13 +119,8 @@ export const getAzureDevopsConnection = async ( export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => { const { credentials: inputCredentials, method } = config; - const { - INF_APP_CONNECTION_AZURE_CLIENT_ID, - INF_APP_CONNECTION_AZURE_CLIENT_SECRET, - INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID, - INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET, - SITE_URL - } = getConfig(); + const { INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID, INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET, SITE_URL } = + getConfig(); switch (method) { case AzureDevOpsConnectionMethod.OAuth: @@ -137,9 +128,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" }); } - const azureClientId = INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; - const azureClientSecret = INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; - if (!azureClientId || !azureClientSecret) { + if (!INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || !INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET) { throw new InternalServerError({ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` }); @@ -156,8 +145,8 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev grant_type: "authorization_code", code: oauthCredentials.code, scope: `https://app.vssps.visualstudio.com/.default`, - client_id: azureClientId, - client_secret: azureClientSecret, + client_id: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID, + client_secret: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET, redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` }) ); diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts index af8ec360c..95102c5d1 100644 --- a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts +++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts @@ -26,11 +26,10 @@ export const getAzureConnectionAccessToken = async ( kmsService: Pick ) => { const appCfg = getConfig(); - const azureClientId = - appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID; - const azureClientSecret = - appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET || appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET; - if (!azureClientId || !azureClientSecret) { + if ( + !appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || + !appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET + ) { throw new BadRequestError({ message: `Azure environment variables have not been configured` }); @@ -61,8 +60,8 @@ export const getAzureConnectionAccessToken = async ( new URLSearchParams({ grant_type: "refresh_token", scope: `openid offline_access`, - client_id: azureClientId, - client_secret: azureClientSecret, + client_id: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, + client_secret: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, refresh_token: credentials.refreshToken }) ); @@ -96,31 +95,23 @@ export const getAzureConnectionAccessToken = async ( }; export const getAzureKeyVaultConnectionListItem = () => { - const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig(); + const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID } = getConfig(); return { name: "Azure Key Vault" as const, app: AppConnection.AzureKeyVault as const, methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth], - oauthClientId: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID + oauthClientId: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID }; }; export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => { const { credentials: inputCredentials, method } = config; - const { - INF_APP_CONNECTION_AZURE_CLIENT_ID, - INF_APP_CONNECTION_AZURE_CLIENT_SECRET, - INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, - INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, - SITE_URL - } = getConfig(); + const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, SITE_URL } = + getConfig(); - const azureClientId = INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || INF_APP_CONNECTION_AZURE_CLIENT_ID; - const azureClientSecret = INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET || INF_APP_CONNECTION_AZURE_CLIENT_SECRET; - - if (!azureClientId || !azureClientSecret) { + if (!INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || !INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET) { throw new InternalServerError({ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured` }); @@ -136,8 +127,8 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK grant_type: "authorization_code", code: inputCredentials.code, scope: `openid offline_access https://vault.azure.net/.default`, - client_id: azureClientId, - client_secret: azureClientSecret, + client_id: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, + client_secret: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback` }) ); From 41ba7edba22cdec974ceaf7c971f2fb4d784e7a2 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 28 Jul 2025 09:50:18 -0700 Subject: [PATCH 67/79] improvement: remove click outside modal close disabling on sync/data source/rotation modals --- .../secret-rotations-v2/CreateSecretRotationV2Modal.tsx | 1 - .../secret-scanning/CreateSecretScanningDataSourceModal.tsx | 1 - frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx index 210257d7d..f5b9a39b3 100644 --- a/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx +++ b/frontend/src/components/secret-rotations-v2/CreateSecretRotationV2Modal.tsx @@ -76,7 +76,6 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
) } - onPointerDownOutside={(e) => e.preventDefault()} className={selectedRotation ? "max-w-2xl" : "max-w-3xl"} subTitle={ selectedRotation ? undefined : "Select a provider to create a secret rotation for." diff --git a/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx b/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx index a3943cf35..c95baaae5 100644 --- a/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx +++ b/frontend/src/components/secret-scanning/CreateSecretScanningDataSourceModal.tsx @@ -75,7 +75,6 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
) } - onPointerDownOutside={(e) => e.preventDefault()} className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"} subTitle={ selectedDataSource ? undefined : "Select a data source to configure secret scanning for." diff --git a/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx b/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx index 7bae0479f..5ff72e810 100644 --- a/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx +++ b/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx @@ -56,7 +56,6 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop "Add Sync" ) } - onPointerDownOutside={(e) => e.preventDefault()} className="max-w-2xl" bodyClassName="overflow-visible" subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."} From 975b621bc8077b46ee2ad6297a38bdf522d75467 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 28 Jul 2025 10:26:22 -0700 Subject: [PATCH 68/79] fix: remove passthrough on banner guard for kms pages --- frontend/src/pages/kms/KmipPage/KmipPage.tsx | 1 - frontend/src/pages/kms/OverviewPage/OverviewPage.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/src/pages/kms/KmipPage/KmipPage.tsx b/frontend/src/pages/kms/KmipPage/KmipPage.tsx index 6297c5eef..9c337bba3 100644 --- a/frontend/src/pages/kms/KmipPage/KmipPage.tsx +++ b/frontend/src/pages/kms/KmipPage/KmipPage.tsx @@ -22,7 +22,6 @@ export const KmipPage = () => { description="Integrate with Infisical KMS via Key Management Interoperability Protocol." /> { description="Manage keys and perform cryptographic operations." /> Date: Tue, 29 Jul 2025 04:56:50 +0800 Subject: [PATCH 69/79] misc: addressed comments --- ...821_add-secret-detection-ignore-values.ts} | 8 +-- backend/src/db/schemas/projects.ts | 2 +- .../secret-approval-request-service.ts | 3 +- .../secret-scanning-v2-fns.ts | 13 ++-- backend/src/lib/api-docs/constants.ts | 2 +- backend/src/lib/config/env.ts | 6 +- backend/src/server/routes/sanitizedSchemas.ts | 2 +- .../src/server/routes/v1/project-router.ts | 7 ++- .../src/services/project/project-service.ts | 6 +- backend/src/services/project/project-types.ts | 2 +- .../secret-v2-bridge-service.ts | 11 ++-- frontend/src/hooks/api/workspace/queries.tsx | 4 +- frontend/src/hooks/api/workspace/types.ts | 4 +- .../ProjectGeneralTab/ProjectGeneralTab.tsx | 4 +- .../SecretDetectionIgnoreValuesSection.tsx} | 60 ++++++++++--------- 15 files changed, 72 insertions(+), 62 deletions(-) rename backend/src/db/migrations/{20250725171821_add-secret-detection-ignore-keys.ts => 20250725171821_add-secret-detection-ignore-values.ts} (73%) rename frontend/src/pages/secret-manager/SettingsPage/components/{SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx => SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx} (65%) diff --git a/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts similarity index 73% rename from backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts rename to backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts index dfdf5ecfc..c8257b771 100644 --- a/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-keys.ts +++ b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts @@ -3,17 +3,17 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { - if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys"))) { + if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues"))) { await knex.schema.alterTable(TableName.Project, (t) => { - t.specificType("secretDetectionIgnoreKeys", "text[]"); + t.specificType("secretDetectionIgnoreValues", "text[]"); }); } } export async function down(knex: Knex): Promise { - if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreKeys")) { + if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues")) { await knex.schema.alterTable(TableName.Project, (t) => { - t.dropColumn("secretDetectionIgnoreKeys"); + t.dropColumn("secretDetectionIgnoreValues"); }); } } diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index fd8b273f2..08dc1eee0 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -31,7 +31,7 @@ export const ProjectsSchema = z.object({ secretSharing: z.boolean().default(true), showSnapshotsLegacy: z.boolean().default(false), defaultProduct: z.string().nullable().optional(), - secretDetectionIgnoreKeys: z.string().array().nullable().optional() + secretDetectionIgnoreValues: z.string().array().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index f0c719802..b5331e897 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -1410,6 +1410,7 @@ export const secretApprovalRequestServiceFactory = ({ const project = await projectDAL.findById(projectId); await scanSecretPolicyViolations( + projectId, secretPath, [ ...(data[SecretOperations.Create] || []), @@ -1418,7 +1419,7 @@ export const secretApprovalRequestServiceFactory = ({ secretKey: el.secretKey, secretValue: el.secretValue as string })), - project.secretDetectionIgnoreKeys || [] + project.secretDetectionIgnoreValues || [] ); // for created secret approval change diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts index 99aa5b91f..bdda63f7a 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts @@ -165,28 +165,29 @@ export const parseScanErrorMessage = (err: unknown): string => { }; export const scanSecretPolicyViolations = async ( + projectId: string, secretPath: string, secrets: { secretKey: string; secretValue: string }[], - ignoreKeys: string[] + ignoreValues: string[] ) => { const appCfg = getConfig(); if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) { return; } - const paramFolderSecretDetectionPaths = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.map((el) => el.secretPath) ?? []; - const isPathMatched = paramFolderSecretDetectionPaths.some((pattern) => - picomatch.isMatch(secretPath, pattern, { strictSlashes: false }) + + const match = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.find( + (el) => el.projectId === projectId && picomatch.isMatch(secretPath, el.secretPath, { strictSlashes: false }) ); - if (!isPathMatched) { + if (!match) { return; } const tempFolder = await createTempFolder(); try { const scanPromises = secrets - .filter((secret) => !ignoreKeys.includes(secret.secretKey)) + .filter((secret) => !ignoreValues.includes(secret.secretValue)) .map(async (secret) => { const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`); await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index f42431195..f3b4cbca0 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -705,7 +705,7 @@ export const PROJECTS = { secretSharing: "Enable or disable secret sharing for the project.", showSnapshotsLegacy: "Enable or disable legacy snapshots for the project.", defaultProduct: "The default product in which the project will open", - secretDetectionIgnoreKeys: "The list of secret keys to ignore for secret detection." + secretDetectionIgnoreValues: "The list of secret values to ignore for secret detection." }, GET_KEY: { workspaceId: "The ID of the project to get the key from." diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index af47a6537..bc32d2b05 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -211,10 +211,9 @@ const envSchema = z .optional() .transform((val) => { if (!val) return undefined; - return JSON.parse(val) as { secretPath: string }[]; + return JSON.parse(val) as { secretPath: string; projectId: string }[]; }) ), - PARAMS_FOLDER_SECRET_DETECTION_ENABLED: zodStrBool.default("false"), // HSM HSM_LIB_PATH: zpStr(z.string().optional()), @@ -353,7 +352,8 @@ const envSchema = z isHsmConfigured: Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined, samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG, - SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",") + SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","), + PARAMS_FOLDER_SECRET_DETECTION_ENABLED: (data.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.length ?? 0) > 0 })); export type TEnvConfig = Readonly>; diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 1564b4bcb..1a69f3845 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -265,7 +265,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ hasDeleteProtection: true, secretSharing: true, showSnapshotsLegacy: true, - secretDetectionIgnoreKeys: true + secretDetectionIgnoreValues: true }); export const SanitizedTagSchema = SecretTagsSchema.pick({ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 5992b1a33..6f981f61f 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -370,7 +370,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing), showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy), defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct), - secretDetectionIgnoreKeys: z.array(z.string()).optional().describe(PROJECTS.UPDATE.secretDetectionIgnoreKeys) + secretDetectionIgnoreValues: z + .array(z.string()) + .optional() + .describe(PROJECTS.UPDATE.secretDetectionIgnoreValues) }), response: { 200: z.object({ @@ -394,7 +397,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { slug: req.body.slug, secretSharing: req.body.secretSharing, showSnapshotsLegacy: req.body.showSnapshotsLegacy, - secretDetectionIgnoreKeys: req.body.secretDetectionIgnoreKeys + secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues }, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index a166d40c3..153f627fc 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -667,9 +667,9 @@ export const projectServiceFactory = ({ } } - if (update.secretDetectionIgnoreKeys && !hasRole(ProjectMembershipRole.Admin)) { + if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) { throw new ForbiddenRequestError({ - message: "Only admins can update secret detection ignore keys" + message: "Only admins can update secret detection ignore values" }); } @@ -683,7 +683,7 @@ export const projectServiceFactory = ({ secretSharing: update.secretSharing, defaultProduct: update.defaultProduct, showSnapshotsLegacy: update.showSnapshotsLegacy, - secretDetectionIgnoreKeys: update.secretDetectionIgnoreKeys + secretDetectionIgnoreValues: update.secretDetectionIgnoreValues }); return updatedProject; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 82f9a0b8f..02f89bc38 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -96,7 +96,7 @@ export type TUpdateProjectDTO = { slug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; - secretDetectionIgnoreKeys?: string[]; + secretDetectionIgnoreValues?: string[]; }; } & Omit; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index f4f815fbd..43daaf0df 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -301,6 +301,7 @@ export const secretV2BridgeServiceFactory = ({ const project = await projectDAL.findById(projectId); await scanSecretPolicyViolations( + projectId, secretPath, [ { @@ -308,7 +309,7 @@ export const secretV2BridgeServiceFactory = ({ secretValue: inputSecret.secretValue } ], - project.secretDetectionIgnoreKeys || [] + project.secretDetectionIgnoreValues || [] ); const { nestedReferences, localReferences } = getAllSecretReferences(inputSecret.secretValue); @@ -525,6 +526,7 @@ export const secretV2BridgeServiceFactory = ({ if (secretValue) { const project = await projectDAL.findById(projectId); await scanSecretPolicyViolations( + projectId, secretPath, [ { @@ -532,7 +534,7 @@ export const secretV2BridgeServiceFactory = ({ secretValue } ], - project.secretDetectionIgnoreKeys || [] + project.secretDetectionIgnoreValues || [] ); } @@ -1616,7 +1618,7 @@ export const secretV2BridgeServiceFactory = ({ throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` }); const project = await projectDAL.findById(projectId); - await scanSecretPolicyViolations(secretPath, inputSecrets, project.secretDetectionIgnoreKeys || []); + await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []); // get all tags const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds); @@ -1960,6 +1962,7 @@ export const secretV2BridgeServiceFactory = ({ const project = await projectDAL.findById(projectId); await scanSecretPolicyViolations( + projectId, secretPath, secretsToUpdate .filter((el) => el.secretValue) @@ -1967,7 +1970,7 @@ export const secretV2BridgeServiceFactory = ({ secretKey: el.newSecretName || el.secretKey, secretValue: el.secretValue as string })), - project.secretDetectionIgnoreKeys || [] + project.secretDetectionIgnoreValues || [] ); const bulkUpdatedSecrets = await fnSecretBulkUpdate({ diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index e4e9d2dd1..7a0e3d2e2 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -282,7 +282,7 @@ export const useUpdateProject = () => { newSlug, secretSharing, showSnapshotsLegacy, - secretDetectionIgnoreKeys + secretDetectionIgnoreValues }) => { const { data } = await apiRequest.patch<{ workspace: Workspace }>( `/api/v1/workspace/${projectID}`, @@ -292,7 +292,7 @@ export const useUpdateProject = () => { slug: newSlug, secretSharing, showSnapshotsLegacy, - secretDetectionIgnoreKeys + secretDetectionIgnoreValues } ); return data.workspace; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index e96b7099e..33eb0909f 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -40,7 +40,7 @@ export type Workspace = { hasDeleteProtection: boolean; secretSharing: boolean; showSnapshotsLegacy: boolean; - secretDetectionIgnoreKeys: string[]; + secretDetectionIgnoreValues: string[]; }; export type WorkspaceEnv = { @@ -82,7 +82,7 @@ export type UpdateProjectDTO = { newSlug?: string; secretSharing?: boolean; showSnapshotsLegacy?: boolean; - secretDetectionIgnoreKeys?: string[]; + secretDetectionIgnoreValues?: string[]; }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx index 022951dff..a542facf5 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -4,7 +4,7 @@ import { AutoCapitalizationSection } from "../AutoCapitalizationSection"; import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection"; import { EnvironmentSection } from "../EnvironmentSection"; import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection"; -import { SecretDetectionIgnoreKeysSection } from "../SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection"; +import { SecretDetectionIgnoreValuesSection } from "../SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection"; import { SecretSharingSection } from "../SecretSharingSection"; import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection"; import { SecretTagsSection } from "../SecretTagsSection"; @@ -20,7 +20,7 @@ export const SecretSettingsTab = () => { - {config.paramsFolderSecretDetectionEnabled && } + {config.paramsFolderSecretDetectionEnabled && }
); diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx similarity index 65% rename from frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx rename to frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx index bc3f04b98..07617000f 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreKeysSection/SecretDetectionIgnoreKeysSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx @@ -12,9 +12,9 @@ import { useUpdateProject } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; const formSchema = z.object({ - ignoreKeys: z + ignoreValues: z .object({ - key: z.string().trim().min(1, "Secret key name is required") + value: z.string().trim().min(1, "Secret value is required") }) .array() .default([]) @@ -22,7 +22,7 @@ const formSchema = z.object({ type TForm = z.infer; -export const SecretDetectionIgnoreKeysSection = () => { +export const SecretDetectionIgnoreValuesSection = () => { const { currentWorkspace } = useWorkspace(); const { membership } = useProjectPermission(); const { mutateAsync: updateProject } = useUpdateProject(); @@ -35,37 +35,39 @@ export const SecretDetectionIgnoreKeysSection = () => { } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - ignoreKeys: [] + ignoreValues: [] } }); - const ignoreKeysFormFields = useFieldArray({ + const ignoreValuesFormFields = useFieldArray({ control, - name: "ignoreKeys" + name: "ignoreValues" }); useEffect(() => { - const existingIgnoreKeys = currentWorkspace?.secretDetectionIgnoreKeys || []; + const existingIgnoreValues = currentWorkspace?.secretDetectionIgnoreValues || []; reset({ - ignoreKeys: - existingIgnoreKeys.length > 0 ? existingIgnoreKeys.map((key) => ({ key })) : [{ key: "" }] // Show one empty field by default + ignoreValues: + existingIgnoreValues.length > 0 + ? existingIgnoreValues.map((value) => ({ value })) + : [{ value: "" }] // Show one empty field by default }); - }, [currentWorkspace?.secretDetectionIgnoreKeys, reset]); + }, [currentWorkspace?.secretDetectionIgnoreValues, reset]); - const handleIgnoreKeysSubmit = async ({ ignoreKeys }: TForm) => { + const handleIgnoreValuesSubmit = async ({ ignoreValues }: TForm) => { try { await updateProject({ projectID: currentWorkspace.id, - secretDetectionIgnoreKeys: ignoreKeys.map((item) => item.key) + secretDetectionIgnoreValues: ignoreValues.map((item) => item.value) }); createNotification({ - text: "Successfully updated secret detection ignore keys", + text: "Successfully updated secret detection ignore values", type: "success" }); } catch { createNotification({ - text: "Failed updating secret detection ignore keys", + text: "Failed updating secret detection ignore values", type: "error" }); } @@ -78,41 +80,41 @@ export const SecretDetectionIgnoreKeysSection = () => { return (
-

Secret Detection Ignore Keys

+

Secret Detection Ignore Values

- Define secret keys that should be ignored when scanning parameter folders for misplaced - secrets. These keys will not trigger policy violation alerts even if they contain sensitive - data. + Define secret values that should be ignored when scanning parameter folders for misplaced + secrets. These values will not trigger policy violation alerts even if they contain + sensitive data.

-
+
-

Ignored Secret Keys

+

Ignored Secret Values

- {ignoreKeysFormFields.fields.map(({ id: ignoreKeyFieldId }, i) => ( -
+ {ignoreValuesFormFields.fields.map(({ id: ignoreValueFieldId }, i) => ( +
- {i === 0 && Secret Key Name} + {i === 0 && Secret Value} ( - + )} />
ignoreKeysFormFields.remove(i)} + onClick={() => ignoreValuesFormFields.remove(i)} isDisabled={!isAdmin} > @@ -124,10 +126,10 @@ export const SecretDetectionIgnoreKeysSection = () => { leftIcon={} size="xs" variant="outline_bg" - onClick={() => ignoreKeysFormFields.append({ key: "" })} + onClick={() => ignoreValuesFormFields.append({ value: "" })} isDisabled={!isAdmin} > - Add Ignore Key + Add Ignore Value
From 122de9960600bf67c4af167b116d24dd39f05a3a Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 28 Jul 2025 15:29:26 -0700 Subject: [PATCH 70/79] improvement: add back secret scanning unresolved finding count to sidebar --- .../SecretScanningLayout.tsx | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx b/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx index c8340e48a..265fcc003 100644 --- a/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx +++ b/frontend/src/layouts/SecretScanningLayout/SecretScanningLayout.tsx @@ -10,8 +10,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, Outlet } from "@tanstack/react-router"; import { motion } from "framer-motion"; -import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; -import { useProjectPermission, useWorkspace } from "@app/context"; +import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { + ProjectPermissionSub, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; +import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types"; +import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secretScanningV2"; import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; @@ -19,6 +26,22 @@ export const SecretScanningLayout = () => { const { currentWorkspace } = useWorkspace(); const { assumedPrivilegeDetails } = useProjectPermission(); + const { permission } = useProjectPermission(); + const { subscription } = useSubscription(); + + const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount( + currentWorkspace.id, + { + enabled: + subscription.secretScanning && + permission.can( + ProjectPermissionSecretScanningFindingActions.Read, + ProjectPermissionSub.SecretScanningFindings + ), + refetchInterval: 30000 + } + ); + return (
@@ -63,11 +86,16 @@ export const SecretScanningLayout = () => { > {({ isActive }) => ( -
+
- Findings + Findings + {Boolean(unresolvedFindings) && ( + + {unresolvedFindings} + + )}
)} From da28f9224b1597e7786792b6899de4b737f29080 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 28 Jul 2025 16:34:39 -0700 Subject: [PATCH 71/79] improvement: improve secret reference styling and reduce debounce for snappier behavior --- .../InfisicalSecretInput.tsx | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index a42ee02a5..ee2f196b1 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -1,5 +1,5 @@ import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react"; -import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons"; +import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; @@ -79,7 +79,7 @@ export const InfisicalSecretInput = forwardRef( const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; - const [debouncedValue] = useDebounce(value, 500); + const [debouncedValue] = useDebounce(value, 100); const [highlightedIndex, setHighlightedIndex] = useState(-1); @@ -142,7 +142,7 @@ export const InfisicalSecretInput = forwardRef( const suggestions = useMemo(() => { if (!isPopupOpen) return []; // reset highlight whenever recomputation happens - setHighlightedIndex(-1); + setHighlightedIndex(0); const suggestionsArr: ReferenceItem[] = []; const predicate = suggestionSource.predicate.toLowerCase(); @@ -298,17 +298,21 @@ export const InfisicalSecretInput = forwardRef( }} >
{suggestions.map((item, i) => { let entryIcon; + let subText; if (item.type === ReferenceType.SECRET) { - entryIcon = faKey; + entryIcon = ; + subText = "Secret"; } else if (item.type === ReferenceType.ENVIRONMENT) { - entryIcon = faCircle; + entryIcon = ; + subText = "Environment"; } else { - entryIcon = faFolder; + entryIcon = ; + subText = "Folder"; } return ( @@ -327,22 +331,22 @@ export const InfisicalSecretInput = forwardRef( }} onMouseEnter={() => setHighlightedIndex(i)} style={{ pointerEvents: "auto" }} - className="flex items-center justify-between border-mineshaft-600 text-left" + className="flex w-full items-center justify-between border-mineshaft-600 text-left" key={`secret-reference-secret-${i + 1}`} >
-
-
- +
+
{entryIcon}
+
+ {item.label} +
+ {subText} +
-
{item.label}
From 7949142ea7dae7034bf2954e7a12c5a182d0a4d4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 28 Jul 2025 23:32:05 -0400 Subject: [PATCH 72/79] update text for secret params --- .../SecretDetectionIgnoreValuesSection.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx index 07617000f..48d0e69e8 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx @@ -80,17 +80,12 @@ export const SecretDetectionIgnoreValuesSection = () => { return (
-

Secret Detection Ignore Values

+

Secret Detection

-

- Define secret values that should be ignored when scanning parameter folders for misplaced - secrets. These values will not trigger policy violation alerts even if they contain - sensitive data. -

+

Define secret values to ignore when scanning designated parameter folders. Add values here to prevent false positives or allow approved sensitive data. These ignored values will not trigger policy violation alerts.

-

Ignored Secret Values

{ignoreValuesFormFields.fields.map(({ id: ignoreValueFieldId }, i) => (
@@ -129,7 +124,7 @@ export const SecretDetectionIgnoreValuesSection = () => { onClick={() => ignoreValuesFormFields.append({ value: "" })} isDisabled={!isAdmin} > - Add Ignore Value + Add value to ignore
From d47f6f7ec999a6fc3c2e9ef1f731ae3abb2d8bd5 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 29 Jul 2025 20:49:54 +0800 Subject: [PATCH 73/79] misc: removed CLI directory --- cli/.gitignore | 4 - cli/.infisicalignore | 3 - cli/agent-config.yaml | 37 - cli/detect/baseline.go | 103 - cli/detect/cmd/scm/scm.go | 70 - cli/detect/config/allowlist.go | 159 - cli/detect/config/config.go | 426 --- cli/detect/config/gitleaks.toml | 3130 ----------------- cli/detect/config/rule.go | 114 - cli/detect/config/utils.go | 46 - cli/detect/decoder.go | 328 -- cli/detect/detect.go | 699 ---- cli/detect/directory.go | 225 -- cli/detect/git.go | 216 -- cli/detect/location.go | 102 - cli/detect/logging/log.go | 72 - cli/detect/reader.go | 149 - cli/detect/regexp/stdlib_regex.go | 37 - cli/detect/regexp/wasilibs_regex.go | 37 - cli/detect/report/constants.go | 26 - cli/detect/report/csv.go | 100 - cli/detect/report/finding.go | 92 - cli/detect/report/json.go | 39 - cli/detect/report/junit.go | 129 - cli/detect/report/report.go | 38 - cli/detect/report/sarif.go | 239 -- cli/detect/report/template.go | 68 - cli/detect/sources/directory.go | 127 - cli/detect/sources/git.go | 211 -- cli/detect/utils.go | 280 -- cli/docker/alpine | 9 - cli/go.mod | 183 - cli/go.sum | 951 ----- cli/goreleaser.dockerfile | 4 - cli/infisical-cli.repo | 5 - cli/main.go | 17 - cli/packages/api/api.go | 652 ---- cli/packages/api/errors.go | 80 - cli/packages/api/model.go | 689 ---- cli/packages/cmd/agent.go | 1081 ------ cli/packages/cmd/bootstrap.go | 277 -- cli/packages/cmd/cmd_test.go | 49 - cli/packages/cmd/dynamic_secrets.go | 676 ---- cli/packages/cmd/export.go | 240 -- cli/packages/cmd/export_test.go | 79 - cli/packages/cmd/folder.go | 209 -- cli/packages/cmd/gateway.go | 318 -- cli/packages/cmd/init.go | 195 - cli/packages/cmd/kmip.go | 103 - cli/packages/cmd/login.go | 1022 ------ cli/packages/cmd/man.go | 35 - .../pre-commit-without-bang.sh | 20 - .../cmd/pre-commit-script/pre-commit.sh | 20 - cli/packages/cmd/reset.go | 45 - cli/packages/cmd/root.go | 104 - cli/packages/cmd/run.go | 491 --- cli/packages/cmd/scan.go | 636 ---- cli/packages/cmd/secrets.go | 782 ---- cli/packages/cmd/ssh.go | 1142 ------ cli/packages/cmd/token.go | 63 - cli/packages/cmd/tokens.go | 193 - cli/packages/cmd/user.go | 325 -- cli/packages/cmd/vault.go | 113 - cli/packages/config/config.go | 5 - cli/packages/crypto/crypto.go | 86 - cli/packages/gateway/connection.go | 358 -- cli/packages/gateway/constants.go | 17 - cli/packages/gateway/gateway.go | 371 -- cli/packages/gateway/relay.go | 188 - cli/packages/gateway/relay_windows.go | 37 - cli/packages/gateway/systemd.go | 121 - .../gateway/udp_listener/listener_unix.go | 26 - .../gateway/udp_listener/listener_windows.go | 18 - cli/packages/models/cli.go | 161 - cli/packages/srp/client.go | 140 - cli/packages/srp/params.go | 95 - cli/packages/srp/server.go | 104 - cli/packages/srp/srp.go | 103 - cli/packages/srp/util.go | 48 - cli/packages/systemd/daemon.go | 84 - cli/packages/telemetry/telemetry.go | 82 - cli/packages/util/agent.go | 41 - cli/packages/util/auth.go | 208 -- cli/packages/util/check-for-update.go | 183 - cli/packages/util/common.go | 117 - cli/packages/util/config.go | 259 -- cli/packages/util/constants.go | 63 - cli/packages/util/credentials.go | 127 - cli/packages/util/exec.go | 92 - cli/packages/util/folders.go | 281 -- cli/packages/util/helper.go | 338 -- cli/packages/util/init.go | 46 - cli/packages/util/keyringwrapper.go | 69 - cli/packages/util/log.go | 49 - cli/packages/util/secrets.go | 824 ----- .../util/testdata/infisical-branch-env.json | 7 - .../util/testdata/infisical-default-env.json | 5 - .../infisical-no-matching-branch-env.json | 7 - cli/packages/util/vault.go | 22 - .../visualize/dynamic_secret_leases.go | 39 - cli/packages/visualize/folders.go | 14 - cli/packages/visualize/secrets.go | 14 - cli/packages/visualize/visualize.go | 134 - cli/scripts/completions.sh | 8 - cli/scripts/export_test_env.sh | 23 - cli/scripts/install.sh | 97 - cli/scripts/manpages.sh | 6 - cli/scripts/setup.deb.sh | 551 --- cli/secret-render-template | 5 - ...-TestServiceToken_ExportSecretsWithImports | 5 - ...stServiceToken_ExportSecretsWithoutImports | 3 - ...TestServiceToken_GetSecretsByNameRecursive | 7 - ...stServiceToken_GetSecretsByNameWithImports | 7 - ...ceToken_GetSecretsByNameWithNotFoundSecret | 8 - ...TestServiceToken_RunCmdRecursiveAndImports | 2 - .../test-TestServiceToken_RunCmdWithImports | 2 - ...test-TestServiceToken_RunCmdWithoutImports | 2 - ...Token_SecretsGetWithImportsAndRecursiveCmd | 10 - ...etsGetWithoutImportsAndWithoutRecursiveCmd | 7 - ...TestUniversalAuth_ExportSecretsWithImports | 5 - ...tUniversalAuth_ExportSecretsWithoutImports | 3 - ...estUniversalAuth_GetSecretsByNameRecursive | 7 - ...tUniversalAuth_GetSecretsByNameWithImports | 7 - ...salAuth_GetSecretsByNameWithNotFoundSecret | 8 - ...estUniversalAuth_RunCmdRecursiveAndImports | 2 - .../test-TestUniversalAuth_RunCmdWithImports | 2 - ...est-TestUniversalAuth_RunCmdWithoutImports | 2 - ...lAuth_SecretsGetWithImportsAndRecursiveCmd | 10 - ...etsGetWithoutImportsAndWithoutRecursiveCmd | 7 - ...stUniversalAuth_SecretsGetWrongEnvironment | 4 - .../test-TestUserAuth_SecretsGetAll | 7 - ...estUserAuth_SecretsGetAllWithoutConnection | 8 - cli/test/export_test.go | 66 - cli/test/helper.go | 107 - cli/test/login_test.go | 139 - cli/test/main_test.go | 23 - cli/test/run_test.go | 108 - cli/test/secrets_by_name_test.go | 94 - cli/test/secrets_test.go | 123 - cli/testdata/baseline/baseline.csv | 2 - cli/testdata/baseline/baseline.json | 40 - cli/testdata/baseline/baseline.sarif | 6 - cli/testdata/config/allow_aws_re.toml | 9 - cli/testdata/config/allow_commit.toml | 9 - cli/testdata/config/allow_global_aws_re.toml | 8 - cli/testdata/config/allow_path.toml | 9 - cli/testdata/config/bad_entropy_group.toml | 8 - cli/testdata/config/base.toml | 10 - cli/testdata/config/entropy_group.toml | 8 - .../config/escaped_character_group.toml | 8 - cli/testdata/config/extend_1.toml | 10 - cli/testdata/config/extend_2.toml | 10 - cli/testdata/config/extend_3.toml | 9 - cli/testdata/config/generic.toml | 8 - cli/testdata/config/generic_with_py_path.toml | 36 - cli/testdata/config/path_only.toml | 6 - cli/testdata/config/simple.toml | 222 -- .../expected/git/small-branch-foo.txt | 17 - cli/testdata/expected/git/small.txt | 67 - cli/testdata/expected/report/csv_simple.csv | 2 - cli/testdata/expected/report/empty.json | 1 - cli/testdata/expected/report/json_simple.json | 22 - .../expected/report/sarif_simple.got.sarif | 302 -- .../expected/report/sarif_simple.sarif | 302 -- cli/testdata/repos/nogit/main.go | 24 - cli/testdata/repos/small/README.md | 2 - cli/testdata/repos/small/api/api.go | 7 - .../repos/small/dotGit/COMMIT_EDITMSG | 1 - cli/testdata/repos/small/dotGit/FETCH_HEAD | 1 - cli/testdata/repos/small/dotGit/HEAD | 1 - cli/testdata/repos/small/dotGit/ORIG_HEAD | 1 - cli/testdata/repos/small/dotGit/config | 13 - cli/testdata/repos/small/dotGit/description | 1 - cli/testdata/repos/small/dotGit/index | Bin 317 -> 0 bytes cli/testdata/repos/small/dotGit/info/exclude | 6 - cli/testdata/repos/small/dotGit/logs/HEAD | 13 - .../small/dotGit/logs/refs/heads/api-pkg | 1 - .../repos/small/dotGit/logs/refs/heads/foo | 3 - .../repos/small/dotGit/logs/refs/heads/main | 2 - .../dotGit/logs/refs/heads/remove-secrets | 3 - .../dotGit/logs/refs/remotes/origin/HEAD | 1 - .../dotGit/logs/refs/remotes/origin/api-pkg | 1 - .../small/dotGit/logs/refs/remotes/origin/foo | 1 - .../dotGit/logs/refs/remotes/origin/main | 1 - .../02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 | 1 - .../15/2888a42422b2ff5868b8d003d626120a9cb738 | Bin 86 -> 0 bytes .../2e/1db472eeba53f06c4026ae4566ea022e36598e | Bin 618 -> 0 bytes .../49/1504d5a31946ce75e22554cc34203d8e5ff3ca | Bin 175 -> 0 bytes .../5c/547e4215d9594c3935bdfefdf4f500016a4112 | Bin 51 -> 0 bytes .../78/9ba677976d5db481de55c799d67acbf8e3f16a | Bin 51 -> 0 bytes .../90/6335481df9a4b48906c90318b4fac76b67fe73 | 3 - .../9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad | Bin 196 -> 0 bytes .../a1/22b33c6bad3ee54724f52f2caad385ab1982ab | Bin 163 -> 0 bytes .../a5/caae6d742e49a33982f1fdc608ce861ea59be5 | Bin 134 -> 0 bytes .../a9/aa0c942dcef669a94f207a77426106b25efd1a | Bin 143 -> 0 bytes .../bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 | Bin 116 -> 0 bytes .../d8/32479114dc6be7207edc7c37ce91dd11b93161 | Bin 80 -> 0 bytes .../da/2622b4d97e32c5801511244b809144b6b3ea78 | Bin 51 -> 0 bytes .../e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb | Bin 143 -> 0 bytes .../f1/b58b97808f8e744f6a23c693859df5b5968901 | Bin 176 -> 0 bytes ...dc2976b84768d0829c75cc8d8fc4d849be62cd.idx | Bin 1324 -> 0 bytes ...c2976b84768d0829c75cc8d8fc4d849be62cd.pack | Bin 2116 -> 0 bytes cli/testdata/repos/small/dotGit/packed-refs | 2 - .../repos/small/dotGit/refs/heads/api-pkg | 1 - .../repos/small/dotGit/refs/heads/foo | 1 - .../repos/small/dotGit/refs/heads/main | 1 - .../small/dotGit/refs/heads/remove-secrets | 1 - .../small/dotGit/refs/remotes/origin/HEAD | 1 - .../small/dotGit/refs/remotes/origin/api-pkg | 1 - .../small/dotGit/refs/remotes/origin/foo | 1 - .../small/dotGit/refs/remotes/origin/main | 1 - cli/testdata/repos/small/main.go | 27 - cli/testdata/repos/staged/.gitleaksignore | 1 - cli/testdata/repos/staged/README.md | 2 - cli/testdata/repos/staged/api/api.go | 10 - .../repos/staged/dotGit/COMMIT_EDITMSG | 1 - cli/testdata/repos/staged/dotGit/FETCH_HEAD | 1 - cli/testdata/repos/staged/dotGit/HEAD | 1 - cli/testdata/repos/staged/dotGit/ORIG_HEAD | 1 - cli/testdata/repos/staged/dotGit/config | 13 - cli/testdata/repos/staged/dotGit/description | 1 - cli/testdata/repos/staged/dotGit/index | Bin 359 -> 0 bytes cli/testdata/repos/staged/dotGit/info/exclude | 6 - cli/testdata/repos/staged/dotGit/logs/HEAD | 14 - .../staged/dotGit/logs/refs/heads/api-pkg | 1 - .../repos/staged/dotGit/logs/refs/heads/foo | 3 - .../repos/staged/dotGit/logs/refs/heads/main | 3 - .../dotGit/logs/refs/heads/remove-secrets | 3 - .../dotGit/logs/refs/remotes/origin/HEAD | 1 - .../dotGit/logs/refs/remotes/origin/api-pkg | 1 - .../dotGit/logs/refs/remotes/origin/foo | 1 - .../dotGit/logs/refs/remotes/origin/main | 1 - .../02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 | 1 - .../15/2888a42422b2ff5868b8d003d626120a9cb738 | Bin 86 -> 0 bytes .../2e/1db472eeba53f06c4026ae4566ea022e36598e | Bin 618 -> 0 bytes .../46/18d7e4512b6b0b1dab85cf846d9f43474ec8be | Bin 44 -> 0 bytes .../49/1504d5a31946ce75e22554cc34203d8e5ff3ca | Bin 175 -> 0 bytes .../5c/547e4215d9594c3935bdfefdf4f500016a4112 | Bin 51 -> 0 bytes .../65/83d6db4a57bbeda62d50fc91649036d499418d | Bin 116 -> 0 bytes .../66/bc70d0c0bfbb6468b3f90c3f1e9f2ddba02b43 | Bin 155 -> 0 bytes .../78/9ba677976d5db481de55c799d67acbf8e3f16a | Bin 51 -> 0 bytes .../90/6335481df9a4b48906c90318b4fac76b67fe73 | 3 - .../9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad | Bin 196 -> 0 bytes .../a1/22b33c6bad3ee54724f52f2caad385ab1982ab | Bin 163 -> 0 bytes .../a5/caae6d742e49a33982f1fdc608ce861ea59be5 | Bin 134 -> 0 bytes .../a9/aa0c942dcef669a94f207a77426106b25efd1a | Bin 143 -> 0 bytes .../b1/6d768dd595a59f947abe087901183d219d7e54 | Bin 182 -> 0 bytes .../bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 | Bin 116 -> 0 bytes .../bf/3f24164d7256b4021575cbdb2f97b98e6f057e | 2 - .../d8/32479114dc6be7207edc7c37ce91dd11b93161 | Bin 80 -> 0 bytes .../da/2622b4d97e32c5801511244b809144b6b3ea78 | Bin 51 -> 0 bytes .../e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb | Bin 143 -> 0 bytes .../f1/b58b97808f8e744f6a23c693859df5b5968901 | Bin 176 -> 0 bytes ...dc2976b84768d0829c75cc8d8fc4d849be62cd.idx | Bin 1324 -> 0 bytes ...c2976b84768d0829c75cc8d8fc4d849be62cd.pack | Bin 2116 -> 0 bytes cli/testdata/repos/staged/dotGit/packed-refs | 2 - .../repos/staged/dotGit/refs/heads/api-pkg | 1 - .../repos/staged/dotGit/refs/heads/foo | 1 - .../repos/staged/dotGit/refs/heads/main | 1 - .../staged/dotGit/refs/heads/remove-secrets | 1 - .../staged/dotGit/refs/remotes/origin/HEAD | 1 - .../staged/dotGit/refs/remotes/origin/api-pkg | 1 - .../staged/dotGit/refs/remotes/origin/foo | 1 - .../staged/dotGit/refs/remotes/origin/main | 1 - cli/testdata/repos/staged/main.go | 27 - .../file_symlink/symlinked_id_ed25519 | 1 - .../repos/symlinks/source_file/id_ed25519 | 7 - cli/testdata/tmp/note.txt | 1 - cli/upload_to_cloudsmith.sh | 21 - 269 files changed, 25965 deletions(-) delete mode 100644 cli/.gitignore delete mode 100644 cli/.infisicalignore delete mode 100644 cli/agent-config.yaml delete mode 100644 cli/detect/baseline.go delete mode 100644 cli/detect/cmd/scm/scm.go delete mode 100644 cli/detect/config/allowlist.go delete mode 100644 cli/detect/config/config.go delete mode 100644 cli/detect/config/gitleaks.toml delete mode 100644 cli/detect/config/rule.go delete mode 100644 cli/detect/config/utils.go delete mode 100644 cli/detect/decoder.go delete mode 100644 cli/detect/detect.go delete mode 100644 cli/detect/directory.go delete mode 100644 cli/detect/git.go delete mode 100644 cli/detect/location.go delete mode 100644 cli/detect/logging/log.go delete mode 100644 cli/detect/reader.go delete mode 100644 cli/detect/regexp/stdlib_regex.go delete mode 100644 cli/detect/regexp/wasilibs_regex.go delete mode 100644 cli/detect/report/constants.go delete mode 100644 cli/detect/report/csv.go delete mode 100644 cli/detect/report/finding.go delete mode 100644 cli/detect/report/json.go delete mode 100644 cli/detect/report/junit.go delete mode 100644 cli/detect/report/report.go delete mode 100644 cli/detect/report/sarif.go delete mode 100644 cli/detect/report/template.go delete mode 100644 cli/detect/sources/directory.go delete mode 100644 cli/detect/sources/git.go delete mode 100644 cli/detect/utils.go delete mode 100644 cli/docker/alpine delete mode 100644 cli/go.mod delete mode 100644 cli/go.sum delete mode 100644 cli/goreleaser.dockerfile delete mode 100644 cli/infisical-cli.repo delete mode 100644 cli/main.go delete mode 100644 cli/packages/api/api.go delete mode 100644 cli/packages/api/errors.go delete mode 100644 cli/packages/api/model.go delete mode 100644 cli/packages/cmd/agent.go delete mode 100644 cli/packages/cmd/bootstrap.go delete mode 100644 cli/packages/cmd/cmd_test.go delete mode 100644 cli/packages/cmd/dynamic_secrets.go delete mode 100644 cli/packages/cmd/export.go delete mode 100644 cli/packages/cmd/export_test.go delete mode 100644 cli/packages/cmd/folder.go delete mode 100644 cli/packages/cmd/gateway.go delete mode 100644 cli/packages/cmd/init.go delete mode 100644 cli/packages/cmd/kmip.go delete mode 100644 cli/packages/cmd/login.go delete mode 100644 cli/packages/cmd/man.go delete mode 100644 cli/packages/cmd/pre-commit-script/pre-commit-without-bang.sh delete mode 100644 cli/packages/cmd/pre-commit-script/pre-commit.sh delete mode 100644 cli/packages/cmd/reset.go delete mode 100644 cli/packages/cmd/root.go delete mode 100644 cli/packages/cmd/run.go delete mode 100644 cli/packages/cmd/scan.go delete mode 100644 cli/packages/cmd/secrets.go delete mode 100644 cli/packages/cmd/ssh.go delete mode 100644 cli/packages/cmd/token.go delete mode 100644 cli/packages/cmd/tokens.go delete mode 100644 cli/packages/cmd/user.go delete mode 100644 cli/packages/cmd/vault.go delete mode 100644 cli/packages/config/config.go delete mode 100644 cli/packages/crypto/crypto.go delete mode 100644 cli/packages/gateway/connection.go delete mode 100644 cli/packages/gateway/constants.go delete mode 100644 cli/packages/gateway/gateway.go delete mode 100644 cli/packages/gateway/relay.go delete mode 100644 cli/packages/gateway/relay_windows.go delete mode 100644 cli/packages/gateway/systemd.go delete mode 100644 cli/packages/gateway/udp_listener/listener_unix.go delete mode 100644 cli/packages/gateway/udp_listener/listener_windows.go delete mode 100644 cli/packages/models/cli.go delete mode 100644 cli/packages/srp/client.go delete mode 100644 cli/packages/srp/params.go delete mode 100644 cli/packages/srp/server.go delete mode 100644 cli/packages/srp/srp.go delete mode 100644 cli/packages/srp/util.go delete mode 100644 cli/packages/systemd/daemon.go delete mode 100644 cli/packages/telemetry/telemetry.go delete mode 100644 cli/packages/util/agent.go delete mode 100644 cli/packages/util/auth.go delete mode 100644 cli/packages/util/check-for-update.go delete mode 100644 cli/packages/util/common.go delete mode 100644 cli/packages/util/config.go delete mode 100644 cli/packages/util/constants.go delete mode 100644 cli/packages/util/credentials.go delete mode 100644 cli/packages/util/exec.go delete mode 100644 cli/packages/util/folders.go delete mode 100644 cli/packages/util/helper.go delete mode 100644 cli/packages/util/init.go delete mode 100644 cli/packages/util/keyringwrapper.go delete mode 100644 cli/packages/util/log.go delete mode 100644 cli/packages/util/secrets.go delete mode 100644 cli/packages/util/testdata/infisical-branch-env.json delete mode 100644 cli/packages/util/testdata/infisical-default-env.json delete mode 100644 cli/packages/util/testdata/infisical-no-matching-branch-env.json delete mode 100644 cli/packages/util/vault.go delete mode 100644 cli/packages/visualize/dynamic_secret_leases.go delete mode 100644 cli/packages/visualize/folders.go delete mode 100644 cli/packages/visualize/secrets.go delete mode 100644 cli/packages/visualize/visualize.go delete mode 100755 cli/scripts/completions.sh delete mode 100644 cli/scripts/export_test_env.sh delete mode 100755 cli/scripts/install.sh delete mode 100755 cli/scripts/manpages.sh delete mode 100644 cli/scripts/setup.deb.sh delete mode 100644 cli/secret-render-template delete mode 100644 cli/test/.snapshots/test-TestServiceToken_ExportSecretsWithImports delete mode 100644 cli/test/.snapshots/test-TestServiceToken_ExportSecretsWithoutImports delete mode 100644 cli/test/.snapshots/test-TestServiceToken_GetSecretsByNameRecursive delete mode 100644 cli/test/.snapshots/test-TestServiceToken_GetSecretsByNameWithImports delete mode 100644 cli/test/.snapshots/test-TestServiceToken_GetSecretsByNameWithNotFoundSecret delete mode 100644 cli/test/.snapshots/test-TestServiceToken_RunCmdRecursiveAndImports delete mode 100644 cli/test/.snapshots/test-TestServiceToken_RunCmdWithImports delete mode 100644 cli/test/.snapshots/test-TestServiceToken_RunCmdWithoutImports delete mode 100644 cli/test/.snapshots/test-TestServiceToken_SecretsGetWithImportsAndRecursiveCmd delete mode 100644 cli/test/.snapshots/test-TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmd delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_ExportSecretsWithImports delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_ExportSecretsWithoutImports delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_GetSecretsByNameRecursive delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_GetSecretsByNameWithImports delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_GetSecretsByNameWithNotFoundSecret delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_RunCmdRecursiveAndImports delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_RunCmdWithImports delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_RunCmdWithoutImports delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_SecretsGetWithImportsAndRecursiveCmd delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmd delete mode 100644 cli/test/.snapshots/test-TestUniversalAuth_SecretsGetWrongEnvironment delete mode 100644 cli/test/.snapshots/test-TestUserAuth_SecretsGetAll delete mode 100644 cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection delete mode 100644 cli/test/export_test.go delete mode 100644 cli/test/helper.go delete mode 100644 cli/test/login_test.go delete mode 100644 cli/test/main_test.go delete mode 100644 cli/test/run_test.go delete mode 100644 cli/test/secrets_by_name_test.go delete mode 100644 cli/test/secrets_test.go delete mode 100644 cli/testdata/baseline/baseline.csv delete mode 100644 cli/testdata/baseline/baseline.json delete mode 100644 cli/testdata/baseline/baseline.sarif delete mode 100644 cli/testdata/config/allow_aws_re.toml delete mode 100644 cli/testdata/config/allow_commit.toml delete mode 100644 cli/testdata/config/allow_global_aws_re.toml delete mode 100644 cli/testdata/config/allow_path.toml delete mode 100755 cli/testdata/config/bad_entropy_group.toml delete mode 100644 cli/testdata/config/base.toml delete mode 100755 cli/testdata/config/entropy_group.toml delete mode 100644 cli/testdata/config/escaped_character_group.toml delete mode 100644 cli/testdata/config/extend_1.toml delete mode 100644 cli/testdata/config/extend_2.toml delete mode 100644 cli/testdata/config/extend_3.toml delete mode 100644 cli/testdata/config/generic.toml delete mode 100644 cli/testdata/config/generic_with_py_path.toml delete mode 100644 cli/testdata/config/path_only.toml delete mode 100644 cli/testdata/config/simple.toml delete mode 100644 cli/testdata/expected/git/small-branch-foo.txt delete mode 100644 cli/testdata/expected/git/small.txt delete mode 100644 cli/testdata/expected/report/csv_simple.csv delete mode 100644 cli/testdata/expected/report/empty.json delete mode 100644 cli/testdata/expected/report/json_simple.json delete mode 100644 cli/testdata/expected/report/sarif_simple.got.sarif delete mode 100644 cli/testdata/expected/report/sarif_simple.sarif delete mode 100644 cli/testdata/repos/nogit/main.go delete mode 100644 cli/testdata/repos/small/README.md delete mode 100644 cli/testdata/repos/small/api/api.go delete mode 100644 cli/testdata/repos/small/dotGit/COMMIT_EDITMSG delete mode 100644 cli/testdata/repos/small/dotGit/FETCH_HEAD delete mode 100644 cli/testdata/repos/small/dotGit/HEAD delete mode 100644 cli/testdata/repos/small/dotGit/ORIG_HEAD delete mode 100644 cli/testdata/repos/small/dotGit/config delete mode 100644 cli/testdata/repos/small/dotGit/description delete mode 100644 cli/testdata/repos/small/dotGit/index delete mode 100644 cli/testdata/repos/small/dotGit/info/exclude delete mode 100644 cli/testdata/repos/small/dotGit/logs/HEAD delete mode 100644 cli/testdata/repos/small/dotGit/logs/refs/heads/api-pkg delete mode 100644 cli/testdata/repos/small/dotGit/logs/refs/heads/foo delete mode 100644 cli/testdata/repos/small/dotGit/logs/refs/heads/main delete mode 100644 cli/testdata/repos/small/dotGit/logs/refs/heads/remove-secrets delete mode 100644 cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/HEAD delete mode 100644 cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/api-pkg delete mode 100644 cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/foo delete mode 100644 cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/main delete mode 100644 cli/testdata/repos/small/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 delete mode 100644 cli/testdata/repos/small/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 delete mode 100644 cli/testdata/repos/small/dotGit/objects/2e/1db472eeba53f06c4026ae4566ea022e36598e delete mode 100644 cli/testdata/repos/small/dotGit/objects/49/1504d5a31946ce75e22554cc34203d8e5ff3ca delete mode 100644 cli/testdata/repos/small/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 delete mode 100644 cli/testdata/repos/small/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a delete mode 100644 cli/testdata/repos/small/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 delete mode 100644 cli/testdata/repos/small/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad delete mode 100644 cli/testdata/repos/small/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab delete mode 100644 cli/testdata/repos/small/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 delete mode 100644 cli/testdata/repos/small/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a delete mode 100644 cli/testdata/repos/small/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 delete mode 100644 cli/testdata/repos/small/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 delete mode 100644 cli/testdata/repos/small/dotGit/objects/da/2622b4d97e32c5801511244b809144b6b3ea78 delete mode 100644 cli/testdata/repos/small/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb delete mode 100644 cli/testdata/repos/small/dotGit/objects/f1/b58b97808f8e744f6a23c693859df5b5968901 delete mode 100644 cli/testdata/repos/small/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.idx delete mode 100644 cli/testdata/repos/small/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.pack delete mode 100644 cli/testdata/repos/small/dotGit/packed-refs delete mode 100644 cli/testdata/repos/small/dotGit/refs/heads/api-pkg delete mode 100644 cli/testdata/repos/small/dotGit/refs/heads/foo delete mode 100644 cli/testdata/repos/small/dotGit/refs/heads/main delete mode 100644 cli/testdata/repos/small/dotGit/refs/heads/remove-secrets delete mode 100644 cli/testdata/repos/small/dotGit/refs/remotes/origin/HEAD delete mode 100644 cli/testdata/repos/small/dotGit/refs/remotes/origin/api-pkg delete mode 100644 cli/testdata/repos/small/dotGit/refs/remotes/origin/foo delete mode 100644 cli/testdata/repos/small/dotGit/refs/remotes/origin/main delete mode 100644 cli/testdata/repos/small/main.go delete mode 100644 cli/testdata/repos/staged/.gitleaksignore delete mode 100644 cli/testdata/repos/staged/README.md delete mode 100644 cli/testdata/repos/staged/api/api.go delete mode 100644 cli/testdata/repos/staged/dotGit/COMMIT_EDITMSG delete mode 100644 cli/testdata/repos/staged/dotGit/FETCH_HEAD delete mode 100644 cli/testdata/repos/staged/dotGit/HEAD delete mode 100644 cli/testdata/repos/staged/dotGit/ORIG_HEAD delete mode 100644 cli/testdata/repos/staged/dotGit/config delete mode 100644 cli/testdata/repos/staged/dotGit/description delete mode 100644 cli/testdata/repos/staged/dotGit/index delete mode 100644 cli/testdata/repos/staged/dotGit/info/exclude delete mode 100644 cli/testdata/repos/staged/dotGit/logs/HEAD delete mode 100644 cli/testdata/repos/staged/dotGit/logs/refs/heads/api-pkg delete mode 100644 cli/testdata/repos/staged/dotGit/logs/refs/heads/foo delete mode 100644 cli/testdata/repos/staged/dotGit/logs/refs/heads/main delete mode 100644 cli/testdata/repos/staged/dotGit/logs/refs/heads/remove-secrets delete mode 100644 cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/HEAD delete mode 100644 cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/api-pkg delete mode 100644 cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/foo delete mode 100644 cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/main delete mode 100644 cli/testdata/repos/staged/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/2e/1db472eeba53f06c4026ae4566ea022e36598e delete mode 100644 cli/testdata/repos/staged/dotGit/objects/46/18d7e4512b6b0b1dab85cf846d9f43474ec8be delete mode 100644 cli/testdata/repos/staged/dotGit/objects/49/1504d5a31946ce75e22554cc34203d8e5ff3ca delete mode 100644 cli/testdata/repos/staged/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/65/83d6db4a57bbeda62d50fc91649036d499418d delete mode 100644 cli/testdata/repos/staged/dotGit/objects/66/bc70d0c0bfbb6468b3f90c3f1e9f2ddba02b43 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a delete mode 100644 cli/testdata/repos/staged/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad delete mode 100644 cli/testdata/repos/staged/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab delete mode 100644 cli/testdata/repos/staged/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a delete mode 100644 cli/testdata/repos/staged/dotGit/objects/b1/6d768dd595a59f947abe087901183d219d7e54 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/bf/3f24164d7256b4021575cbdb2f97b98e6f057e delete mode 100644 cli/testdata/repos/staged/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/da/2622b4d97e32c5801511244b809144b6b3ea78 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb delete mode 100644 cli/testdata/repos/staged/dotGit/objects/f1/b58b97808f8e744f6a23c693859df5b5968901 delete mode 100644 cli/testdata/repos/staged/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.idx delete mode 100644 cli/testdata/repos/staged/dotGit/objects/pack/pack-2cdc2976b84768d0829c75cc8d8fc4d849be62cd.pack delete mode 100644 cli/testdata/repos/staged/dotGit/packed-refs delete mode 100644 cli/testdata/repos/staged/dotGit/refs/heads/api-pkg delete mode 100644 cli/testdata/repos/staged/dotGit/refs/heads/foo delete mode 100644 cli/testdata/repos/staged/dotGit/refs/heads/main delete mode 100644 cli/testdata/repos/staged/dotGit/refs/heads/remove-secrets delete mode 100644 cli/testdata/repos/staged/dotGit/refs/remotes/origin/HEAD delete mode 100644 cli/testdata/repos/staged/dotGit/refs/remotes/origin/api-pkg delete mode 100644 cli/testdata/repos/staged/dotGit/refs/remotes/origin/foo delete mode 100644 cli/testdata/repos/staged/dotGit/refs/remotes/origin/main delete mode 100644 cli/testdata/repos/staged/main.go delete mode 120000 cli/testdata/repos/symlinks/file_symlink/symlinked_id_ed25519 delete mode 100644 cli/testdata/repos/symlinks/source_file/id_ed25519 delete mode 100644 cli/testdata/tmp/note.txt delete mode 100755 cli/upload_to_cloudsmith.sh diff --git a/cli/.gitignore b/cli/.gitignore deleted file mode 100644 index 8eb54d72b..000000000 --- a/cli/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.infisical.json -dist/ -agent-config.test.yaml -.test.env \ No newline at end of file diff --git a/cli/.infisicalignore b/cli/.infisicalignore deleted file mode 100644 index e5dfe29bc..000000000 --- a/cli/.infisicalignore +++ /dev/null @@ -1,3 +0,0 @@ -bea0ff6e05a4de73a5db625d4ae181a015b50855:frontend/components/utilities/attemptLogin.js:stripe-access-token:147 -bea0ff6e05a4de73a5db625d4ae181a015b50855:backend/src/json/integrations.json:generic-api-key:5 -1961b92340e5d2613acae528b886c842427ce5d0:frontend/components/utilities/attemptLogin.js:stripe-access-token:148 diff --git a/cli/agent-config.yaml b/cli/agent-config.yaml deleted file mode 100644 index 210c21413..000000000 --- a/cli/agent-config.yaml +++ /dev/null @@ -1,37 +0,0 @@ -infisical: - address: "https://app.infisical.com/" -auth: - type: "universal-auth" - config: - client-id: "./client-id" - client-secret: "./client-secret" - remove_client_secret_on_read: false -sinks: - - type: "file" - config: - path: "access-token" -templates: - - template-content: | - {{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }} - {{- range . }} - {{ .Key }}={{ .Value }} - {{- end }} - {{- end }} - destination-path: my-dot-env-0.env - config: - polling-interval: 60s - execute: - command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d - - - base64-template-content: e3stIHdpdGggc2VjcmV0ICIyMDJmMDRkNy1lNGNiLTQzZDQtYTI5Mi1lODkzNzEyZDYxZmMiICJkZXYiICIvIiB9fQp7ey0gcmFuZ2UgLiB9fQp7eyAuS2V5IH19PXt7IC5WYWx1ZSB9fQp7ey0gZW5kIH19Cnt7LSBlbmQgfX0= - destination-path: my-dot-env.env - config: - polling-interval: 60s - execute: - command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d - - - source-path: my-dot-ev-secret-template1 - destination-path: my-dot-env-1.env - config: - exec: - command: mkdir hello-world1 diff --git a/cli/detect/baseline.go b/cli/detect/baseline.go deleted file mode 100644 index eeaa2a73a..000000000 --- a/cli/detect/baseline.go +++ /dev/null @@ -1,103 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package detect - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/Infisical/infisical-merge/detect/report" -) - -func IsNew(finding report.Finding, redact uint, baseline []report.Finding) bool { - // Explicitly testing each property as it gives significantly better performance in comparison to cmp.Equal(). Drawback is that - // the code requires maintenance if/when the Finding struct changes - for _, b := range baseline { - if finding.RuleID == b.RuleID && - finding.Description == b.Description && - finding.StartLine == b.StartLine && - finding.EndLine == b.EndLine && - finding.StartColumn == b.StartColumn && - finding.EndColumn == b.EndColumn && - (redact > 0 || (finding.Match == b.Match && finding.Secret == b.Secret)) && - finding.File == b.File && - finding.Commit == b.Commit && - finding.Author == b.Author && - finding.Email == b.Email && - finding.Date == b.Date && - finding.Message == b.Message && - // Omit checking finding.Fingerprint - if the format of the fingerprint changes, the users will see unexpected behaviour - finding.Entropy == b.Entropy { - return false - } - } - return true -} - -func LoadBaseline(baselinePath string) ([]report.Finding, error) { - bytes, err := os.ReadFile(baselinePath) - if err != nil { - return nil, fmt.Errorf("could not open %s", baselinePath) - } - - var previousFindings []report.Finding - err = json.Unmarshal(bytes, &previousFindings) - if err != nil { - return nil, fmt.Errorf("the format of the file %s is not supported", baselinePath) - } - - return previousFindings, nil -} - -func (d *Detector) AddBaseline(baselinePath string, source string) error { - if baselinePath != "" { - absoluteSource, err := filepath.Abs(source) - if err != nil { - return err - } - - absoluteBaseline, err := filepath.Abs(baselinePath) - if err != nil { - return err - } - - relativeBaseline, err := filepath.Rel(absoluteSource, absoluteBaseline) - if err != nil { - return err - } - - baseline, err := LoadBaseline(baselinePath) - if err != nil { - return err - } - - d.baseline = baseline - baselinePath = relativeBaseline - - } - - d.baselinePath = baselinePath - return nil -} diff --git a/cli/detect/cmd/scm/scm.go b/cli/detect/cmd/scm/scm.go deleted file mode 100644 index dddeffdf5..000000000 --- a/cli/detect/cmd/scm/scm.go +++ /dev/null @@ -1,70 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package scm - -import ( - "fmt" - "strings" -) - -type Platform int - -const ( - UnknownPlatform Platform = iota - NoPlatform // Explicitly disable the feature - GitHubPlatform - GitLabPlatform - AzureDevOpsPlatform - BitBucketPlatform - // TODO: Add others. -) - -func (p Platform) String() string { - return [...]string{ - "unknown", - "none", - "github", - "gitlab", - "azuredevops", - "bitbucket", - }[p] -} - -func PlatformFromString(s string) (Platform, error) { - switch strings.ToLower(s) { - case "", "unknown": - return UnknownPlatform, nil - case "none": - return NoPlatform, nil - case "github": - return GitHubPlatform, nil - case "gitlab": - return GitLabPlatform, nil - case "azuredevops": - return AzureDevOpsPlatform, nil - case "bitbucket": - return BitBucketPlatform, nil - default: - return UnknownPlatform, fmt.Errorf("invalid scm platform value: %s", s) - } -} diff --git a/cli/detect/config/allowlist.go b/cli/detect/config/allowlist.go deleted file mode 100644 index d91188f68..000000000 --- a/cli/detect/config/allowlist.go +++ /dev/null @@ -1,159 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package config - -import ( - "fmt" - "strings" - - "golang.org/x/exp/maps" - - "github.com/Infisical/infisical-merge/detect/regexp" -) - -type AllowlistMatchCondition int - -const ( - AllowlistMatchOr AllowlistMatchCondition = iota - AllowlistMatchAnd -) - -func (a AllowlistMatchCondition) String() string { - return [...]string{ - "OR", - "AND", - }[a] -} - -// Allowlist allows a rule to be ignored for specific -// regexes, paths, and/or commits -type Allowlist struct { - // Short human readable description of the allowlist. - Description string - - // MatchCondition determines whether all criteria must match. - MatchCondition AllowlistMatchCondition - - // Commits is a slice of commit SHAs that are allowed to be ignored. Defaults to "OR". - Commits []string - - // Paths is a slice of path regular expressions that are allowed to be ignored. - Paths []*regexp.Regexp - - // Can be `match` or `line`. - // - // If `match` the _Regexes_ will be tested against the match of the _Rule.Regex_. - // - // If `line` the _Regexes_ will be tested against the entire line. - // - // If RegexTarget is empty, it will be tested against the found secret. - RegexTarget string - - // Regexes is slice of content regular expressions that are allowed to be ignored. - Regexes []*regexp.Regexp - - // StopWords is a slice of stop words that are allowed to be ignored. - // This targets the _secret_, not the content of the regex match like the - // Regexes slice. - StopWords []string - - // validated is an internal flag to track whether `Validate()` has been called. - validated bool -} - -func (a *Allowlist) Validate() error { - if a.validated { - return nil - } - - // Disallow empty allowlists. - if len(a.Commits) == 0 && - len(a.Paths) == 0 && - len(a.Regexes) == 0 && - len(a.StopWords) == 0 { - return fmt.Errorf("must contain at least one check for: commits, paths, regexes, or stopwords") - } - - // Deduplicate commits and stopwords. - if len(a.Commits) > 0 { - uniqueCommits := make(map[string]struct{}) - for _, commit := range a.Commits { - uniqueCommits[commit] = struct{}{} - } - a.Commits = maps.Keys(uniqueCommits) - } - if len(a.StopWords) > 0 { - uniqueStopwords := make(map[string]struct{}) - for _, stopWord := range a.StopWords { - uniqueStopwords[stopWord] = struct{}{} - } - a.StopWords = maps.Keys(uniqueStopwords) - } - - a.validated = true - return nil -} - -// CommitAllowed returns true if the commit is allowed to be ignored. -func (a *Allowlist) CommitAllowed(c string) (bool, string) { - if a == nil || c == "" { - return false, "" - } - - for _, commit := range a.Commits { - if commit == c { - return true, c - } - } - return false, "" -} - -// PathAllowed returns true if the path is allowed to be ignored. -func (a *Allowlist) PathAllowed(path string) bool { - if a == nil || path == "" { - return false - } - return anyRegexMatch(path, a.Paths) -} - -// RegexAllowed returns true if the regex is allowed to be ignored. -func (a *Allowlist) RegexAllowed(secret string) bool { - if a == nil || secret == "" { - return false - } - return anyRegexMatch(secret, a.Regexes) -} - -func (a *Allowlist) ContainsStopWord(s string) (bool, string) { - if a == nil || s == "" { - return false, "" - } - - s = strings.ToLower(s) - for _, stopWord := range a.StopWords { - if strings.Contains(s, strings.ToLower(stopWord)) { - return true, stopWord - } - } - return false, "" -} diff --git a/cli/detect/config/config.go b/cli/detect/config/config.go deleted file mode 100644 index 10c6db7e0..000000000 --- a/cli/detect/config/config.go +++ /dev/null @@ -1,426 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package config - -import ( - _ "embed" - "errors" - "fmt" - "sort" - "strings" - - "github.com/spf13/viper" - - "github.com/Infisical/infisical-merge/detect/logging" - "github.com/Infisical/infisical-merge/detect/regexp" -) - -const DefaultScanConfigFileName = ".infisical-scan.toml" -const DefaultScanConfigEnvName = "INFISICAL_SCAN_CONFIG" -const DefaultInfisicalIgnoreFineName = ".infisicalignore" - -var ( - //go:embed gitleaks.toml - DefaultConfig string - - // use to keep track of how many configs we can extend - // yea I know, globals bad - extendDepth int -) - -const maxExtendDepth = 2 - -// ViperConfig is the config struct used by the Viper config package -// to parse the config file. This struct does not include regular expressions. -// It is used as an intermediary to convert the Viper config to the Config struct. -type ViperConfig struct { - Title string - Description string - Extend Extend - Rules []struct { - ID string - Description string - Path string - Regex string - SecretGroup int - Entropy float64 - Keywords []string - Tags []string - - // Deprecated: this is a shim for backwards-compatibility. - // TODO: Remove this in 9.x. - AllowList *viperRuleAllowlist - Allowlists []*viperRuleAllowlist - } - // Deprecated: this is a shim for backwards-compatibility. - // TODO: Remove this in 9.x. - AllowList *viperGlobalAllowlist - Allowlists []*viperGlobalAllowlist -} - -type viperRuleAllowlist struct { - Description string - Condition string - Commits []string - Paths []string - RegexTarget string - Regexes []string - StopWords []string -} - -type viperGlobalAllowlist struct { - TargetRules []string - viperRuleAllowlist `mapstructure:",squash"` -} - -// Config is a configuration struct that contains rules and an allowlist if present. -type Config struct { - Title string - Extend Extend - Path string - Description string - Rules map[string]Rule - Keywords map[string]struct{} - // used to keep sarif results consistent - OrderedRules []string - Allowlists []*Allowlist -} - -// Extend is a struct that allows users to define how they want their -// configuration extended by other configuration files. -type Extend struct { - Path string - URL string - UseDefault bool - DisabledRules []string -} - -func (vc *ViperConfig) Translate() (Config, error) { - var ( - keywords = make(map[string]struct{}) - orderedRules []string - rulesMap = make(map[string]Rule) - ruleAllowlists = make(map[string][]*Allowlist) - ) - - // Validate individual rules. - for _, vr := range vc.Rules { - var ( - pathPat *regexp.Regexp - regexPat *regexp.Regexp - ) - if vr.Path != "" { - pathPat = regexp.MustCompile(vr.Path) - } - if vr.Regex != "" { - regexPat = regexp.MustCompile(vr.Regex) - } - if vr.Keywords == nil { - vr.Keywords = []string{} - } else { - for i, k := range vr.Keywords { - keyword := strings.ToLower(k) - keywords[keyword] = struct{}{} - vr.Keywords[i] = keyword - } - } - if vr.Tags == nil { - vr.Tags = []string{} - } - cr := Rule{ - RuleID: vr.ID, - Description: vr.Description, - Regex: regexPat, - SecretGroup: vr.SecretGroup, - Entropy: vr.Entropy, - Path: pathPat, - Keywords: vr.Keywords, - Tags: vr.Tags, - } - - // Parse the rule allowlists, including the older format for backwards compatibility. - if vr.AllowList != nil { - // TODO: Remove this in v9. - if len(vr.Allowlists) > 0 { - return Config{}, fmt.Errorf("%s: [rules.allowlist] is deprecated, it cannot be used alongside [[rules.allowlist]]", cr.RuleID) - } - vr.Allowlists = append(vr.Allowlists, vr.AllowList) - } - for _, a := range vr.Allowlists { - allowlist, err := parseAllowlist(a) - if err != nil { - return Config{}, fmt.Errorf("%s: [[rules.allowlists]] %w", cr.RuleID, err) - } - cr.Allowlists = append(cr.Allowlists, allowlist) - } - orderedRules = append(orderedRules, cr.RuleID) - rulesMap[cr.RuleID] = cr - } - - // Assemble the config. - c := Config{ - Title: vc.Title, - Description: vc.Description, - Extend: vc.Extend, - Rules: rulesMap, - Keywords: keywords, - OrderedRules: orderedRules, - } - // Parse the config allowlists, including the older format for backwards compatibility. - if vc.AllowList != nil { - // TODO: Remove this in v9. - if len(vc.Allowlists) > 0 { - return Config{}, errors.New("[allowlist] is deprecated, it cannot be used alongside [[allowlists]]") - } - vc.Allowlists = append(vc.Allowlists, vc.AllowList) - } - for _, a := range vc.Allowlists { - allowlist, err := parseAllowlist(&a.viperRuleAllowlist) - if err != nil { - return Config{}, fmt.Errorf("[[allowlists]] %w", err) - } - // Allowlists with |targetRules| aren't added to the global list. - if len(a.TargetRules) > 0 { - for _, ruleID := range a.TargetRules { - // It's not possible to validate |ruleID| until after extend. - ruleAllowlists[ruleID] = append(ruleAllowlists[ruleID], allowlist) - } - } else { - c.Allowlists = append(c.Allowlists, allowlist) - } - } - - if maxExtendDepth != extendDepth { - // disallow both usedefault and path from being set - if c.Extend.Path != "" && c.Extend.UseDefault { - return Config{}, errors.New("unable to load config due to extend.path and extend.useDefault being set") - } - if c.Extend.UseDefault { - if err := c.extendDefault(); err != nil { - return Config{}, err - } - } else if c.Extend.Path != "" { - if err := c.extendPath(); err != nil { - return Config{}, err - } - } - } - - // Validate the rules after everything has been assembled (including extended configs). - if extendDepth == 0 { - for _, rule := range c.Rules { - if err := rule.Validate(); err != nil { - return Config{}, err - } - } - - // Populate targeted configs. - for ruleID, allowlists := range ruleAllowlists { - rule, ok := c.Rules[ruleID] - if !ok { - return Config{}, fmt.Errorf("[[allowlists]] target rule ID '%s' does not exist", ruleID) - } - rule.Allowlists = append(rule.Allowlists, allowlists...) - c.Rules[ruleID] = rule - } - } - - return c, nil -} - -func parseAllowlist(a *viperRuleAllowlist) (*Allowlist, error) { - var matchCondition AllowlistMatchCondition - switch strings.ToUpper(a.Condition) { - case "AND", "&&": - matchCondition = AllowlistMatchAnd - case "", "OR", "||": - matchCondition = AllowlistMatchOr - default: - return nil, fmt.Errorf("unknown allowlist |condition| '%s' (expected 'and', 'or')", a.Condition) - } - - // Validate the target. - regexTarget := a.RegexTarget - if regexTarget != "" { - switch regexTarget { - case "secret": - regexTarget = "" - case "match", "line": - // do nothing - default: - return nil, fmt.Errorf("unknown allowlist |regexTarget| '%s' (expected 'match', 'line')", regexTarget) - } - } - var allowlistRegexes []*regexp.Regexp - for _, a := range a.Regexes { - allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a)) - } - var allowlistPaths []*regexp.Regexp - for _, a := range a.Paths { - allowlistPaths = append(allowlistPaths, regexp.MustCompile(a)) - } - - allowlist := &Allowlist{ - Description: a.Description, - MatchCondition: matchCondition, - Commits: a.Commits, - Paths: allowlistPaths, - RegexTarget: regexTarget, - Regexes: allowlistRegexes, - StopWords: a.StopWords, - } - if err := allowlist.Validate(); err != nil { - return nil, err - } - return allowlist, nil -} - -func (c *Config) GetOrderedRules() []Rule { - var orderedRules []Rule - for _, id := range c.OrderedRules { - if _, ok := c.Rules[id]; ok { - orderedRules = append(orderedRules, c.Rules[id]) - } - } - return orderedRules -} - -func (c *Config) extendDefault() error { - extendDepth++ - viper.SetConfigType("toml") - if err := viper.ReadConfig(strings.NewReader(DefaultConfig)); err != nil { - return fmt.Errorf("failed to load extended default config, err: %w", err) - } - defaultViperConfig := ViperConfig{} - if err := viper.Unmarshal(&defaultViperConfig); err != nil { - return fmt.Errorf("failed to load extended default config, err: %w", err) - } - cfg, err := defaultViperConfig.Translate() - if err != nil { - return fmt.Errorf("failed to load extended default config, err: %w", err) - - } - logging.Debug().Msg("extending config with default config") - c.extend(cfg) - return nil -} - -func (c *Config) extendPath() error { - extendDepth++ - viper.SetConfigFile(c.Extend.Path) - if err := viper.ReadInConfig(); err != nil { - return fmt.Errorf("failed to load extended config, err: %w", err) - } - extensionViperConfig := ViperConfig{} - if err := viper.Unmarshal(&extensionViperConfig); err != nil { - return fmt.Errorf("failed to load extended config, err: %w", err) - } - cfg, err := extensionViperConfig.Translate() - if err != nil { - return fmt.Errorf("failed to load extended config, err: %w", err) - } - logging.Debug().Msgf("extending config with %s", c.Extend.Path) - c.extend(cfg) - return nil -} - -func (c *Config) extendURL() { - // TODO -} - -func (c *Config) extend(extensionConfig Config) { - // Get config name for helpful log messages. - var configName string - if c.Extend.Path != "" { - configName = c.Extend.Path - } else { - configName = "default" - } - // Convert |Config.DisabledRules| into a map for ease of access. - disabledRuleIDs := map[string]struct{}{} - for _, id := range c.Extend.DisabledRules { - if _, ok := extensionConfig.Rules[id]; !ok { - logging.Warn(). - Str("rule-id", id). - Str("config", configName). - Msg("Disabled rule doesn't exist in extended config.") - } - disabledRuleIDs[id] = struct{}{} - } - - for ruleID, baseRule := range extensionConfig.Rules { - // Skip the rule. - if _, ok := disabledRuleIDs[ruleID]; ok { - logging.Debug(). - Str("rule-id", ruleID). - Str("config", configName). - Msg("Ignoring rule from extended config.") - continue - } - - currentRule, ok := c.Rules[ruleID] - if !ok { - // Rule doesn't exist, add it to the config. - c.Rules[ruleID] = baseRule - for _, k := range baseRule.Keywords { - c.Keywords[k] = struct{}{} - } - c.OrderedRules = append(c.OrderedRules, ruleID) - } else { - // Rule exists, merge our changes into the base. - if currentRule.Description != "" { - baseRule.Description = currentRule.Description - } - if currentRule.Entropy != 0 { - baseRule.Entropy = currentRule.Entropy - } - if currentRule.SecretGroup != 0 { - baseRule.SecretGroup = currentRule.SecretGroup - } - if currentRule.Regex != nil { - baseRule.Regex = currentRule.Regex - } - if currentRule.Path != nil { - baseRule.Path = currentRule.Path - } - baseRule.Tags = append(baseRule.Tags, currentRule.Tags...) - baseRule.Keywords = append(baseRule.Keywords, currentRule.Keywords...) - for _, a := range currentRule.Allowlists { - baseRule.Allowlists = append(baseRule.Allowlists, a) - } - // The keywords from the base rule and the extended rule must be merged into the global keywords list - for _, k := range baseRule.Keywords { - c.Keywords[k] = struct{}{} - } - c.Rules[ruleID] = baseRule - } - } - - // append allowlists, not attempting to merge - for _, a := range extensionConfig.Allowlists { - c.Allowlists = append(c.Allowlists, a) - } - - // sort to keep extended rules in order - sort.Strings(c.OrderedRules) -} diff --git a/cli/detect/config/gitleaks.toml b/cli/detect/config/gitleaks.toml deleted file mode 100644 index 92a06a319..000000000 --- a/cli/detect/config/gitleaks.toml +++ /dev/null @@ -1,3130 +0,0 @@ -# This file has been auto-generated. Do not edit manually. -# If you would like to contribute new rules, please use -# cmd/generate/config/main.go and follow the contributing guidelines -# at https://github.com/gitleaks/gitleaks/blob/master/CONTRIBUTING.md -# -# How the hell does secret scanning work? Read this: -# https://lookingatcomputer.substack.com/p/regex-is-almost-all-you-need -# -# This is the default gitleaks configuration file. -# Rules and allowlists are defined within this file. -# Rules instruct gitleaks on what should be considered a secret. -# Allowlists instruct gitleaks on what is allowed, i.e. not a secret. - -title = "gitleaks config" - -# TODO: change to [[allowlists]] -[allowlist] -description = "global allow lists" -paths = [ - '''gitleaks\.toml''', - '''(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$''', - '''(?i)\.(?:eot|[ot]tf|woff2?)$''', - '''(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf|zip)$''', - '''go\.(?:mod|sum|work(?:\.sum)?)$''', - '''(?:^|/)vendor/modules\.txt$''', - '''(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$''', - '''(?:^|/)gradlew(?:\.bat)?$''', - '''(?:^|/)gradle\.lockfile$''', - '''(?:^|/)mvnw(?:\.cmd)?$''', - '''(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$''', - '''(?:^|/)node_modules(?:/.*)?$''', - '''(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$''', - '''(?:^|/)bower_components(?:/.*)?$''', - '''(?:^|/)(?:angular|bootstrap|jquery(?:-?ui)?|plotly|swagger-?ui)[a-zA-Z0-9.-]*(?:\.min)?\.js(?:\.map)?$''', - '''(?:^|/)javascript\.json$''', - '''(?:^|/)(?:Pipfile|poetry)\.lock$''', - '''(?i)(?:^|/)(?:v?env|virtualenv)/lib(?:64)?(?:/.*)?$''', - '''(?i)(?:^|/)(?:lib(?:64)?/python[23](?:\.\d{1,2})+|python/[23](?:\.\d{1,2})+/lib(?:64)?)(?:/.*)?$''', - '''(?i)(?:^|/)[a-z0-9_.]+-[0-9.]+\.dist-info(?:/.+)?$''', - '''(?:^|/)vendor/(?:bundle|ruby)(?:/.*?)?$''', - '''\.gem$''', - '''verification-metadata\.xml''', - '''Database.refactorlog''', -] -regexes = [ - '''(?i)^true|false|null$''', - '''^(?i:a+|b+|c+|d+|e+|f+|g+|h+|i+|j+|k+|l+|m+|n+|o+|p+|q+|r+|s+|t+|u+|v+|w+|x+|y+|z+|\*+|\.+)$''', - '''^\$(?:\d+|{\d+})$''', - '''^\$(?:[A-Z_]+|[a-z_]+)$''', - '''^\${(?:[A-Z_]+|[a-z_]+)}$''', - '''^\{\{[ \t]*[\w ().|]+[ \t]*}}$''', - '''^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$''', - '''^%(?:[A-Z_]+|[a-z_]+)%$''', - '''^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$''', - '''^\{\d{0,2}}$''', - '''^@(?:[A-Z_]+|[a-z_]+)@$''', - '''^/Users/(?i)[a-z0-9]+/[\w .-/]+$''', - '''^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$''', -] -stopwords = [ - "abcdefghijklmnopqrstuvwxyz", - "014df517-39d1-4453-b7b3-9930c563627c", -] - -[[rules]] -id = "1password-secret-key" -description = "Uncovered a possible 1Password secret key, potentially compromising access to secrets in vaults." -regex = '''\bA3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\b''' -entropy = 3.8 -keywords = ["a3-"] - -[[rules]] -id = "1password-service-account-token" -description = "Uncovered a possible 1Password service account token, potentially compromising access to secrets in vaults." -regex = '''ops_eyJ[a-zA-Z0-9+/]{250,}={0,3}''' -entropy = 4 -keywords = ["ops_"] - -[[rules]] -id = "adafruit-api-key" -description = "Identified a potential Adafruit API Key, which could lead to unauthorized access to Adafruit services and sensitive data exposure." -regex = '''(?i)[\w.-]{0,50}?(?:adafruit)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["adafruit"] - -[[rules]] -id = "adobe-client-id" -description = "Detected a pattern that resembles an Adobe OAuth Web Client ID, posing a risk of compromised Adobe integrations and data breaches." -regex = '''(?i)[\w.-]{0,50}?(?:adobe)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["adobe"] - -[[rules]] -id = "adobe-client-secret" -description = "Discovered a potential Adobe Client Secret, which, if exposed, could allow unauthorized Adobe service access and data manipulation." -regex = '''\b(p8e-(?i)[a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["p8e-"] - -[[rules]] -id = "age-secret-key" -description = "Discovered a potential Age encryption tool secret key, risking data decryption and unauthorized access to sensitive information." -regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}''' -keywords = ["age-secret-key-1"] - -[[rules]] -id = "airtable-api-key" -description = "Uncovered a possible Airtable API Key, potentially compromising database access and leading to data leakage or alteration." -regex = '''(?i)[\w.-]{0,50}?(?:airtable)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{17})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["airtable"] - -[[rules]] -id = "algolia-api-key" -description = "Identified an Algolia API Key, which could result in unauthorized search operations and data exposure on Algolia-managed platforms." -regex = '''(?i)[\w.-]{0,50}?(?:algolia)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["algolia"] - -[[rules]] -id = "alibaba-access-key-id" -description = "Detected an Alibaba Cloud AccessKey ID, posing a risk of unauthorized cloud resource access and potential data compromise." -regex = '''\b(LTAI(?i)[a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["ltai"] - -[[rules]] -id = "alibaba-secret-key" -description = "Discovered a potential Alibaba Cloud Secret Key, potentially allowing unauthorized operations and data access within Alibaba Cloud." -regex = '''(?i)[\w.-]{0,50}?(?:alibaba)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["alibaba"] - -[[rules]] -id = "asana-client-id" -description = "Discovered a potential Asana Client ID, risking unauthorized access to Asana projects and sensitive task information." -regex = '''(?i)[\w.-]{0,50}?(?:asana)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["asana"] - -[[rules]] -id = "asana-client-secret" -description = "Identified an Asana Client Secret, which could lead to compromised project management integrity and unauthorized access." -regex = '''(?i)[\w.-]{0,50}?(?:asana)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["asana"] - -[[rules]] -id = "atlassian-api-token" -description = "Detected an Atlassian API token, posing a threat to project management and collaboration tool security and data confidentiality." -regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:atlassian|confluence|jira)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-zA-Z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)|\b(ATATT3[A-Za-z0-9_\-=]{186})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3.5 -keywords = [ - "atlassian", - "confluence", - "jira", - "atatt3", -] - -[[rules]] -id = "authress-service-client-access-key" -description = "Uncovered a possible Authress Service Client Access Key, which may compromise access control services and sensitive data." -regex = '''\b((?:sc|ext|scauth|authress)_(?i)[a-z0-9]{5,30}\.[a-z0-9]{4,6}\.(?-i:acc)[_-][a-z0-9-]{10,32}\.[a-z0-9+/_=-]{30,120})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = [ - "sc_", - "ext_", - "scauth_", - "authress_", -] - -[[rules]] -id = "aws-access-token" -description = "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms." -regex = '''\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16})\b''' -entropy = 3 -keywords = [ - "a3t", - "akia", - "asia", - "abia", - "acca", -] -[[rules.allowlists]] -regexes = [ - '''.+EXAMPLE$''', -] - -[[rules]] -id = "azure-ad-client-secret" -description = "Azure AD Client Secret" -regex = '''(?:^|[\\'"\x60\s>=:(,)])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\\'"\x60\s<),])''' -entropy = 3 -keywords = ["q~"] - -[[rules]] -id = "beamer-api-token" -description = "Detected a Beamer API token, potentially compromising content management and exposing sensitive notifications and updates." -regex = '''(?i)[\w.-]{0,50}?(?:beamer)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(b_[a-z0-9=_\-]{44})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["beamer"] - -[[rules]] -id = "bitbucket-client-id" -description = "Discovered a potential Bitbucket Client ID, risking unauthorized repository access and potential codebase exposure." -regex = '''(?i)[\w.-]{0,50}?(?:bitbucket)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["bitbucket"] - -[[rules]] -id = "bitbucket-client-secret" -description = "Discovered a potential Bitbucket Client Secret, posing a risk of compromised code repositories and unauthorized access." -regex = '''(?i)[\w.-]{0,50}?(?:bitbucket)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["bitbucket"] - -[[rules]] -id = "bittrex-access-key" -description = "Identified a Bittrex Access Key, which could lead to unauthorized access to cryptocurrency trading accounts and financial loss." -regex = '''(?i)[\w.-]{0,50}?(?:bittrex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["bittrex"] - -[[rules]] -id = "bittrex-secret-key" -description = "Detected a Bittrex Secret Key, potentially compromising cryptocurrency transactions and financial security." -regex = '''(?i)[\w.-]{0,50}?(?:bittrex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["bittrex"] - -[[rules]] -id = "cisco-meraki-api-key" -description = "Cisco Meraki is a cloud-managed IT solution that provides networking, security, and device management through an easy-to-use interface." -regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Mm]eraki|MERAKI))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["meraki"] - -[[rules]] -id = "clickhouse-cloud-api-secret-key" -description = "Identified a pattern that may indicate clickhouse cloud API secret key, risking unauthorized clickhouse cloud api access and data breaches on ClickHouse Cloud platforms." -regex = '''\b(4b1d[A-Za-z0-9]{38})\b''' -entropy = 3 -keywords = ["4b1d"] - -[[rules]] -id = "clojars-api-token" -description = "Uncovered a possible Clojars API token, risking unauthorized access to Clojure libraries and potential code manipulation." -regex = '''(?i)CLOJARS_[a-z0-9]{60}''' -entropy = 2 -keywords = ["clojars_"] - -[[rules]] -id = "cloudflare-api-key" -description = "Detected a Cloudflare API Key, potentially compromising cloud application deployments and operational security." -regex = '''(?i)[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["cloudflare"] - -[[rules]] -id = "cloudflare-global-api-key" -description = "Detected a Cloudflare Global API Key, potentially compromising cloud application deployments and operational security." -regex = '''(?i)[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{37})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["cloudflare"] - -[[rules]] -id = "cloudflare-origin-ca-key" -description = "Detected a Cloudflare Origin CA Key, potentially compromising cloud application deployments and operational security." -regex = '''\b(v1\.0-[a-f0-9]{24}-[a-f0-9]{146})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = [ - "cloudflare", - "v1.0-", -] - -[[rules]] -id = "codecov-access-token" -description = "Found a pattern resembling a Codecov Access Token, posing a risk of unauthorized access to code coverage reports and sensitive data." -regex = '''(?i)[\w.-]{0,50}?(?:codecov)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["codecov"] - -[[rules]] -id = "cohere-api-token" -description = "Identified a Cohere Token, posing a risk of unauthorized access to AI services and data manipulation." -regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-zA-Z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 4 -keywords = [ - "cohere", - "co_api_key", -] - -[[rules]] -id = "coinbase-access-token" -description = "Detected a Coinbase Access Token, posing a risk of unauthorized access to cryptocurrency accounts and financial transactions." -regex = '''(?i)[\w.-]{0,50}?(?:coinbase)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["coinbase"] - -[[rules]] -id = "confluent-access-token" -description = "Identified a Confluent Access Token, which could compromise access to streaming data platforms and sensitive data flow." -regex = '''(?i)[\w.-]{0,50}?(?:confluent)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["confluent"] - -[[rules]] -id = "confluent-secret-key" -description = "Found a Confluent Secret Key, potentially risking unauthorized operations and data access within Confluent services." -regex = '''(?i)[\w.-]{0,50}?(?:confluent)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["confluent"] - -[[rules]] -id = "contentful-delivery-api-token" -description = "Discovered a Contentful delivery API token, posing a risk to content management systems and data integrity." -regex = '''(?i)[\w.-]{0,50}?(?:contentful)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{43})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["contentful"] - -[[rules]] -id = "curl-auth-header" -description = "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource." -regex = '''\bcurl\b(?:.*?|.*?(?:[\r\n]{1,2}.*?){1,5})[ \t\n\r](?:-H|--header)(?:=|[ \t]{0,5})(?:"(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))"|'(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))')(?:\B|\s|\z)''' -entropy = 2.75 -keywords = ["curl"] - -[[rules]] -id = "curl-auth-user" -description = "Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource." -regex = '''\bcurl\b(?:.*|.*(?:[\r\n]{1,2}.*){1,5})[ \t\n\r](?:-u|--user)(?:=|[ \t]{0,5})("(:[^"]{3,}|[^:"]{3,}:|[^:"]{3,}:[^"]{3,})"|'([^:']{3,}:[^']{3,})'|((?:"[^"]{3,}"|'[^']{3,}'|[\w$@.-]+):(?:"[^"]{3,}"|'[^']{3,}'|[\w${}@.-]+)))(?:\s|\z)''' -entropy = 2 -keywords = ["curl"] -[[rules.allowlists]] -regexes = [ - '''[^:]+:(?:change(?:it|me)|pass(?:word)?|pwd|test|token|\*+|x+)''', - '''['"]?<[^>]+>['"]?:['"]?<[^>]+>|<[^:]+:[^>]+>['"]?''', - '''[^:]+:\[[^]]+]''', - '''['"]?[^:]+['"]?:['"]?\$(?:\d|\w+|\{(?:\d|\w+)})['"]?''', - '''\$\([^)]+\):\$\([^)]+\)''', - '''['"]?\$?{{[^}]+}}['"]?:['"]?\$?{{[^}]+}}['"]?''', -] - -[[rules]] -id = "databricks-api-token" -description = "Uncovered a Databricks API token, which may compromise big data analytics platforms and sensitive data processing." -regex = '''\b(dapi[a-f0-9]{32}(?:-\d)?)(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["dapi"] - -[[rules]] -id = "datadog-access-token" -description = "Detected a Datadog Access Token, potentially risking monitoring and analytics data exposure and manipulation." -regex = '''(?i)[\w.-]{0,50}?(?:datadog)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["datadog"] - -[[rules]] -id = "defined-networking-api-token" -description = "Identified a Defined Networking API token, which could lead to unauthorized network operations and data breaches." -regex = '''(?i)[\w.-]{0,50}?(?:dnkey)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(dnkey-[a-z0-9=_\-]{26}-[a-z0-9=_\-]{52})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["dnkey"] - -[[rules]] -id = "digitalocean-access-token" -description = "Found a DigitalOcean OAuth Access Token, risking unauthorized cloud resource access and data compromise." -regex = '''\b(doo_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["doo_v1_"] - -[[rules]] -id = "digitalocean-pat" -description = "Discovered a DigitalOcean Personal Access Token, posing a threat to cloud infrastructure security and data privacy." -regex = '''\b(dop_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["dop_v1_"] - -[[rules]] -id = "digitalocean-refresh-token" -description = "Uncovered a DigitalOcean OAuth Refresh Token, which could allow prolonged unauthorized access and resource manipulation." -regex = '''(?i)\b(dor_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["dor_v1_"] - -[[rules]] -id = "discord-api-token" -description = "Detected a Discord API key, potentially compromising communication channels and user data privacy on Discord." -regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["discord"] - -[[rules]] -id = "discord-client-id" -description = "Identified a Discord client ID, which may lead to unauthorized integrations and data exposure in Discord applications." -regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{18})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["discord"] - -[[rules]] -id = "discord-client-secret" -description = "Discovered a potential Discord client secret, risking compromised Discord bot integrations and data leaks." -regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["discord"] - -[[rules]] -id = "doppler-api-token" -description = "Discovered a Doppler API token, posing a risk to environment and secrets management security." -regex = '''dp\.pt\.(?i)[a-z0-9]{43}''' -entropy = 2 -keywords = ["dp.pt."] - -[[rules]] -id = "droneci-access-token" -description = "Detected a Droneci Access Token, potentially compromising continuous integration and deployment workflows." -regex = '''(?i)[\w.-]{0,50}?(?:droneci)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["droneci"] - -[[rules]] -id = "dropbox-api-token" -description = "Identified a Dropbox API secret, which could lead to unauthorized file access and data breaches in Dropbox storage." -regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{15})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["dropbox"] - -[[rules]] -id = "dropbox-long-lived-api-token" -description = "Found a Dropbox long-lived API token, risking prolonged unauthorized access to cloud storage and sensitive data." -regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["dropbox"] - -[[rules]] -id = "dropbox-short-lived-api-token" -description = "Discovered a Dropbox short-lived API token, posing a risk of temporary but potentially harmful data access and manipulation." -regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(sl\.[a-z0-9\-=_]{135})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["dropbox"] - -[[rules]] -id = "duffel-api-token" -description = "Uncovered a Duffel API token, which may compromise travel platform integrations and sensitive customer data." -regex = '''duffel_(?:test|live)_(?i)[a-z0-9_\-=]{43}''' -entropy = 2 -keywords = ["duffel_"] - -[[rules]] -id = "dynatrace-api-token" -description = "Detected a Dynatrace API token, potentially risking application performance monitoring and data exposure." -regex = '''dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}''' -entropy = 4 -keywords = ["dt0c01."] - -[[rules]] -id = "easypost-api-token" -description = "Identified an EasyPost API token, which could lead to unauthorized postal and shipment service access and data exposure." -regex = '''\bEZAK(?i)[a-z0-9]{54}\b''' -entropy = 2 -keywords = ["ezak"] - -[[rules]] -id = "easypost-test-api-token" -description = "Detected an EasyPost test API token, risking exposure of test environments and potentially sensitive shipment data." -regex = '''\bEZTK(?i)[a-z0-9]{54}\b''' -entropy = 2 -keywords = ["eztk"] - -[[rules]] -id = "etsy-access-token" -description = "Found an Etsy Access Token, potentially compromising Etsy shop management and customer data." -regex = '''(?i)[\w.-]{0,50}?(?:(?-i:ETSY|[Ee]tsy))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["etsy"] - -[[rules]] -id = "facebook-access-token" -description = "Discovered a Facebook Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure." -regex = '''(?i)\b(\d{15,16}(\||%)[0-9a-z\-_]{27,40})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["facebook"] - -[[rules]] -id = "facebook-page-access-token" -description = "Discovered a Facebook Page Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure." -regex = '''\b(EAA[MC](?i)[a-z0-9]{100,})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 4 -keywords = [ - "eaam", - "eaac", -] - -[[rules]] -id = "facebook-secret" -description = "Discovered a Facebook Application secret, posing a risk of unauthorized access to Facebook accounts and personal data exposure." -regex = '''(?i)[\w.-]{0,50}?(?:facebook)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["facebook"] - -[[rules]] -id = "fastly-api-token" -description = "Uncovered a Fastly API key, which may compromise CDN and edge cloud services, leading to content delivery and security issues." -regex = '''(?i)[\w.-]{0,50}?(?:fastly)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["fastly"] - -[[rules]] -id = "finicity-api-token" -description = "Detected a Finicity API token, potentially risking financial data access and unauthorized financial operations." -regex = '''(?i)[\w.-]{0,50}?(?:finicity)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["finicity"] - -[[rules]] -id = "finicity-client-secret" -description = "Identified a Finicity Client Secret, which could lead to compromised financial service integrations and data breaches." -regex = '''(?i)[\w.-]{0,50}?(?:finicity)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["finicity"] - -[[rules]] -id = "finnhub-access-token" -description = "Found a Finnhub Access Token, risking unauthorized access to financial market data and analytics." -regex = '''(?i)[\w.-]{0,50}?(?:finnhub)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["finnhub"] - -[[rules]] -id = "flickr-access-token" -description = "Discovered a Flickr Access Token, posing a risk of unauthorized photo management and potential data leakage." -regex = '''(?i)[\w.-]{0,50}?(?:flickr)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["flickr"] - -[[rules]] -id = "flutterwave-encryption-key" -description = "Uncovered a Flutterwave Encryption Key, which may compromise payment processing and sensitive financial information." -regex = '''FLWSECK_TEST-(?i)[a-h0-9]{12}''' -entropy = 2 -keywords = ["flwseck_test"] - -[[rules]] -id = "flutterwave-public-key" -description = "Detected a Finicity Public Key, potentially exposing public cryptographic operations and integrations." -regex = '''FLWPUBK_TEST-(?i)[a-h0-9]{32}-X''' -entropy = 2 -keywords = ["flwpubk_test"] - -[[rules]] -id = "flutterwave-secret-key" -description = "Identified a Flutterwave Secret Key, risking unauthorized financial transactions and data breaches." -regex = '''FLWSECK_TEST-(?i)[a-h0-9]{32}-X''' -entropy = 2 -keywords = ["flwseck_test"] - -[[rules]] -id = "flyio-access-token" -description = "Uncovered a Fly.io API key" -regex = '''\b((?:fo1_[\w-]{43}|fm1[ar]_[a-zA-Z0-9+\/]{100,}={0,3}|fm2_[a-zA-Z0-9+\/]{100,}={0,3}))(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 4 -keywords = [ - "fo1_", - "fm1", - "fm2_", -] - -[[rules]] -id = "frameio-api-token" -description = "Found a Frame.io API token, potentially compromising video collaboration and project management." -regex = '''fio-u-(?i)[a-z0-9\-_=]{64}''' -keywords = ["fio-u-"] - -[[rules]] -id = "freemius-secret-key" -description = "Detected a Freemius secret key, potentially exposing sensitive information." -regex = '''(?i)["']secret_key["']\s*=>\s*["'](sk_[\S]{29})["']''' -path = '''(?i)\.php$''' -keywords = ["secret_key"] - -[[rules]] -id = "freshbooks-access-token" -description = "Discovered a Freshbooks Access Token, posing a risk to accounting software access and sensitive financial data exposure." -regex = '''(?i)[\w.-]{0,50}?(?:freshbooks)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["freshbooks"] - -[[rules]] -id = "gcp-api-key" -description = "Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches." -regex = '''\b(AIza[\w-]{35})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 4 -keywords = ["aiza"] -[[rules.allowlists]] -regexes = [ - '''AIzaSyabcdefghijklmnopqrstuvwxyz1234567''', - '''AIzaSyAnLA7NfeLquW1tJFpx_eQCxoX-oo6YyIs''', - '''AIzaSyCkEhVjf3pduRDt6d1yKOMitrUEke8agEM''', - '''AIzaSyDMAScliyLx7F0NPDEJi1QmyCgHIAODrlU''', - '''AIzaSyD3asb-2pEZVqMkmL6M9N6nHZRR_znhrh0''', - '''AIzayDNSXIbFmlXbIE6mCzDLQAqITYefhixbX4A''', - '''AIzaSyAdOS2zB6NCsk1pCdZ4-P6GBdi_UUPwX7c''', - '''AIzaSyASWm6HmTMdYWpgMnjRBjxcQ9CKctWmLd4''', - '''AIzaSyANUvH9H9BsUccjsu2pCmEkOPjjaXeDQgY''', - '''AIzaSyA5_iVawFQ8ABuTZNUdcwERLJv_a_p4wtM''', - '''AIzaSyA4UrcGxgwQFTfaI3no3t7Lt1sjmdnP5sQ''', - '''AIzaSyDSb51JiIcB6OJpwwMicseKRhhrOq1cS7g''', - '''AIzaSyBF2RrAIm4a0mO64EShQfqfd2AFnzAvvuU''', - '''AIzaSyBcE-OOIbhjyR83gm4r2MFCu4MJmprNXsw''', - '''AIzaSyB8qGxt4ec15vitgn44duC5ucxaOi4FmqE''', - '''AIzaSyA8vmApnrHNFE0bApF4hoZ11srVL_n0nvY''', -] - -[[rules]] -id = "generic-api-key" -description = "Detected a Generic API Key, potentially exposing access to various services and sensitive operations." -regex = '''(?i)[\w.-]{0,50}?(?:access|auth|(?-i:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3.5 -keywords = [ - "access", - "api", - "auth", - "key", - "credential", - "creds", - "passwd", - "password", - "secret", - "token", -] -[[rules.allowlists]] -regexes = [ - '''^[a-zA-Z_.-]+$''', -] -[[rules.allowlists]] -description = "Allowlist for Generic API Keys" -regexTarget = "match" -regexes = [ - '''(?i)(?:access(?:ibility|or)|access[_.-]?id|random[_.-]?access|api[_.-]?(?:id|name|version)|rapid|capital|[a-z0-9-]*?api[a-z0-9-]*?:jar:|author|X-MS-Exchange-Organization-Auth|Authentication-Results|(?:credentials?[_.-]?id|withCredentials)|(?:bucket|foreign|hot|idx|natural|primary|pub(?:lic)?|schema|sequence)[_.-]?key|(?:turkey)|key[_.-]?(?:alias|board|code|frame|id|length|mesh|name|pair|press(?:ed)?|ring|selector|signature|size|stone|storetype|word|up|down|left|right)|key[_.-]?vault[_.-]?(?:id|name)|keyVaultToStoreSecrets|key(?:store|tab)[_.-]?(?:file|path)|issuerkeyhash|(?-i:[DdMm]onkey|[DM]ONKEY)|keying|(?:secret)[_.-]?(?:length|name|size)|UserSecretsId|(?:csrf)[_.-]?token|(?:io\.jsonwebtoken[ \t]?:[ \t]?[\w-]+)|(?:api|credentials|token)[_.-]?(?:endpoint|ur[il])|public[_.-]?token|(?:key|token)[_.-]?file|(?-i:(?:[A-Z_]+=\n[A-Z_]+=|[a-z_]+=\n[a-z_]+=)(?:\n|\z))|(?-i:(?:[A-Z.]+=\n[A-Z.]+=|[a-z.]+=\n[a-z.]+=)(?:\n|\z)))''', -] -stopwords = [ - "000000", - "6fe4476ee5a1832882e326b506d14126", - "_ec2_", - "aaaaaa", - "about", - "abstract", - "academy", - "acces", - "account", - "act-", - "act.", - "act_", - "action", - "active", - "actively", - "activity", - "adapter", - "add-", - "add-on", - "add.", - "add_", - "addon", - "addres", - "admin", - "adobe", - "advanced", - "adventure", - "agent", - "agile", - "air-", - "air.", - "air_", - "ajax", - "akka", - "alert", - "alfred", - "algorithm", - "all-", - "all.", - "all_", - "alloy", - "alpha", - "amazon", - "amqp", - "analysi", - "analytic", - "analyzer", - "android", - "angular", - "angularj", - "animate", - "animation", - "another", - "ansible", - "answer", - "ant-", - "ant.", - "ant_", - "any-", - "any.", - "any_", - "apache", - "app-", - "app.", - "app_", - "apple", - "arch", - "archive", - "archived", - "arduino", - "array", - "art-", - "art.", - "art_", - "article", - "asp-", - "asp.", - "asp_", - "asset", - "async", - "atom", - "attention", - "audio", - "audit", - "aura", - "auth", - "author", - "authorize", - "auto", - "automated", - "automatic", - "awesome", - "aws_", - "azure", - "back", - "backbone", - "backend", - "backup", - "bar-", - "bar.", - "bar_", - "base", - "based", - "bash", - "basic", - "batch", - "been", - "beer", - "behavior", - "being", - "benchmark", - "best", - "beta", - "better", - "big-", - "big.", - "big_", - "binary", - "binding", - "bit-", - "bit.", - "bit_", - "bitcoin", - "block", - "blog", - "board", - "book", - "bookmark", - "boost", - "boot", - "bootstrap", - "bosh", - "bot-", - "bot.", - "bot_", - "bower", - "box-", - "box.", - "box_", - "boxen", - "bracket", - "branch", - "bridge", - "browser", - "brunch", - "buffer", - "bug-", - "bug.", - "bug_", - "build", - "builder", - "building", - "buildout", - "buildpack", - "built", - "bundle", - "busines", - "but-", - "but.", - "but_", - "button", - "cache", - "caching", - "cakephp", - "calendar", - "call", - "camera", - "campfire", - "can-", - "can.", - "can_", - "canva", - "captcha", - "capture", - "card", - "carousel", - "case", - "cassandra", - "cat-", - "cat.", - "cat_", - "category", - "center", - "cento", - "challenge", - "change", - "changelog", - "channel", - "chart", - "chat", - "cheat", - "check", - "checker", - "chef", - "ches", - "chinese", - "chosen", - "chrome", - "ckeditor", - "clas", - "classe", - "classic", - "clean", - "cli-", - "cli.", - "cli_", - "client", - "clojure", - "clone", - "closure", - "cloud", - "club", - "cluster", - "cms-", - "cms_", - "coco", - "code", - "coding", - "coffee", - "color", - "combination", - "combo", - "command", - "commander", - "comment", - "commit", - "common", - "community", - "compas", - "compiler", - "complete", - "component", - "composer", - "computer", - "computing", - "con-", - "con.", - "con_", - "concept", - "conf", - "config", - "connect", - "connector", - "console", - "contact", - "container", - "contao", - "content", - "contest", - "context", - "control", - "convert", - "converter", - "conway'", - "cookbook", - "cookie", - "cool", - "copy", - "cordova", - "core", - "couchbase", - "couchdb", - "countdown", - "counter", - "course", - "craft", - "crawler", - "create", - "creating", - "creator", - "credential", - "crm-", - "crm.", - "crm_", - "cros", - "crud", - "csv-", - "csv.", - "csv_", - "cube", - "cucumber", - "cuda", - "current", - "currently", - "custom", - "daemon", - "dark", - "dart", - "dash", - "dashboard", - "data", - "database", - "date", - "day-", - "day.", - "day_", - "dead", - "debian", - "debug", - "debugger", - "deck", - "define", - "del-", - "del.", - "del_", - "delete", - "demo", - "deploy", - "design", - "designer", - "desktop", - "detection", - "detector", - "dev-", - "dev.", - "dev_", - "develop", - "developer", - "device", - "devise", - "diff", - "digital", - "directive", - "directory", - "discovery", - "display", - "django", - "dns-", - "dns_", - "doc-", - "doc.", - "doc_", - "docker", - "docpad", - "doctrine", - "document", - "doe-", - "doe.", - "doe_", - "dojo", - "dom-", - "dom.", - "dom_", - "domain", - "don't", - "done", - "dot-", - "dot.", - "dot_", - "dotfile", - "download", - "draft", - "drag", - "drill", - "drive", - "driven", - "driver", - "drop", - "dropbox", - "drupal", - "dsl-", - "dsl.", - "dsl_", - "dynamic", - "easy", - "ecdsa", - "eclipse", - "edit", - "editing", - "edition", - "editor", - "element", - "emac", - "email", - "embed", - "embedded", - "ember", - "emitter", - "emulator", - "encoding", - "endpoint", - "engine", - "english", - "enhanced", - "entity", - "entry", - "env_", - "episode", - "erlang", - "error", - "espresso", - "event", - "evented", - "example", - "exchange", - "exercise", - "experiment", - "expire", - "exploit", - "explorer", - "export", - "exporter", - "expres", - "ext-", - "ext.", - "ext_", - "extended", - "extension", - "external", - "extra", - "extractor", - "fabric", - "facebook", - "factory", - "fake", - "fast", - "feature", - "feed", - "fewfwef", - "ffmpeg", - "field", - "file", - "filter", - "find", - "finder", - "firefox", - "firmware", - "first", - "fish", - "fix-", - "fix_", - "flash", - "flask", - "flat", - "flex", - "flexible", - "flickr", - "flow", - "fluent", - "fluentd", - "fluid", - "folder", - "font", - "force", - "foreman", - "fork", - "form", - "format", - "formatter", - "forum", - "foundry", - "framework", - "free", - "friend", - "friendly", - "front-end", - "frontend", - "ftp-", - "ftp.", - "ftp_", - "fuel", - "full", - "fun-", - "fun.", - "fun_", - "func", - "future", - "gaia", - "gallery", - "game", - "gateway", - "gem-", - "gem.", - "gem_", - "gen-", - "gen.", - "gen_", - "general", - "generator", - "generic", - "genetic", - "get-", - "get.", - "get_", - "getenv", - "getting", - "ghost", - "gist", - "git-", - "git.", - "git_", - "github", - "gitignore", - "gitlab", - "glas", - "gmail", - "gnome", - "gnu-", - "gnu.", - "gnu_", - "goal", - "golang", - "gollum", - "good", - "google", - "gpu-", - "gpu.", - "gpu_", - "gradle", - "grail", - "graph", - "graphic", - "great", - "grid", - "groovy", - "group", - "grunt", - "guard", - "gui-", - "gui.", - "gui_", - "guide", - "guideline", - "gulp", - "gwt-", - "gwt.", - "gwt_", - "hack", - "hackathon", - "hacker", - "hacking", - "hadoop", - "haml", - "handler", - "hardware", - "has-", - "has_", - "hash", - "haskell", - "have", - "haxe", - "hello", - "help", - "helper", - "here", - "hero", - "heroku", - "high", - "hipchat", - "history", - "home", - "homebrew", - "homepage", - "hook", - "host", - "hosting", - "hot-", - "hot.", - "hot_", - "house", - "how-", - "how.", - "how_", - "html", - "http", - "hub-", - "hub.", - "hub_", - "hubot", - "human", - "icon", - "ide-", - "ide.", - "ide_", - "idea", - "identity", - "idiomatic", - "image", - "impact", - "import", - "important", - "importer", - "impres", - "index", - "infinite", - "info", - "injection", - "inline", - "input", - "inside", - "inspector", - "instagram", - "install", - "installer", - "instant", - "intellij", - "interface", - "internet", - "interview", - "into", - "intro", - "ionic", - "iphone", - "ipython", - "irc-", - "irc_", - "iso-", - "iso.", - "iso_", - "issue", - "jade", - "jasmine", - "java", - "jbos", - "jekyll", - "jenkin", - "jetbrains", - "job-", - "job.", - "job_", - "joomla", - "jpa-", - "jpa.", - "jpa_", - "jquery", - "json", - "just", - "kafka", - "karma", - "kata", - "kernel", - "keyboard", - "kindle", - "kit-", - "kit.", - "kit_", - "kitchen", - "knife", - "koan", - "kohana", - "lab-", - "lab.", - "lab_", - "lambda", - "lamp", - "language", - "laravel", - "last", - "latest", - "latex", - "launcher", - "layer", - "layout", - "lazy", - "ldap", - "leaflet", - "league", - "learn", - "learning", - "led-", - "led.", - "led_", - "leetcode", - "les-", - "les.", - "les_", - "level", - "leveldb", - "lib-", - "lib.", - "lib_", - "librarie", - "library", - "license", - "life", - "liferay", - "light", - "lightbox", - "like", - "line", - "link", - "linked", - "linkedin", - "linux", - "lisp", - "list", - "lite", - "little", - "load", - "loader", - "local", - "location", - "lock", - "log-", - "log.", - "log_", - "logger", - "logging", - "logic", - "login", - "logstash", - "longer", - "look", - "love", - "lua-", - "lua.", - "lua_", - "mac-", - "mac.", - "mac_", - "machine", - "made", - "magento", - "magic", - "mail", - "make", - "maker", - "making", - "man-", - "man.", - "man_", - "manage", - "manager", - "manifest", - "manual", - "map-", - "map.", - "map_", - "mapper", - "mapping", - "markdown", - "markup", - "master", - "math", - "matrix", - "maven", - "md5", - "mean", - "media", - "mediawiki", - "meetup", - "memcached", - "memory", - "menu", - "merchant", - "message", - "messaging", - "meta", - "metadata", - "meteor", - "method", - "metric", - "micro", - "middleman", - "migration", - "minecraft", - "miner", - "mini", - "minimal", - "mirror", - "mit-", - "mit.", - "mit_", - "mobile", - "mocha", - "mock", - "mod-", - "mod.", - "mod_", - "mode", - "model", - "modern", - "modular", - "module", - "modx", - "money", - "mongo", - "mongodb", - "mongoid", - "mongoose", - "monitor", - "monkey", - "more", - "motion", - "moved", - "movie", - "mozilla", - "mqtt", - "mule", - "multi", - "multiple", - "music", - "mustache", - "mvc-", - "mvc.", - "mvc_", - "mysql", - "nagio", - "name", - "native", - "need", - "neo-", - "neo.", - "neo_", - "nest", - "nested", - "net-", - "net.", - "net_", - "nette", - "network", - "new-", - "new.", - "new_", - "next", - "nginx", - "ninja", - "nlp-", - "nlp.", - "nlp_", - "node", - "nodej", - "nosql", - "not-", - "not.", - "not_", - "note", - "notebook", - "notepad", - "notice", - "notifier", - "now-", - "now.", - "now_", - "number", - "oauth", - "object", - "objective", - "obsolete", - "ocaml", - "octopres", - "official", - "old-", - "old.", - "old_", - "onboard", - "online", - "only", - "open", - "opencv", - "opengl", - "openshift", - "openwrt", - "option", - "oracle", - "org-", - "org.", - "org_", - "origin", - "original", - "orm-", - "orm.", - "orm_", - "osx-", - "osx_", - "our-", - "our.", - "our_", - "out-", - "out.", - "out_", - "output", - "over", - "overview", - "own-", - "own.", - "own_", - "pack", - "package", - "packet", - "page", - "panel", - "paper", - "paperclip", - "para", - "parallax", - "parallel", - "parse", - "parser", - "parsing", - "particle", - "party", - "password", - "patch", - "path", - "pattern", - "payment", - "paypal", - "pdf-", - "pdf.", - "pdf_", - "pebble", - "people", - "perl", - "personal", - "phalcon", - "phoenix", - "phone", - "phonegap", - "photo", - "php-", - "php.", - "php_", - "physic", - "picker", - "pipeline", - "platform", - "play", - "player", - "please", - "plu-", - "plu.", - "plu_", - "plug-in", - "plugin", - "plupload", - "png-", - "png.", - "png_", - "poker", - "polyfill", - "polymer", - "pool", - "pop-", - "pop.", - "pop_", - "popcorn", - "popup", - "port", - "portable", - "portal", - "portfolio", - "post", - "power", - "powered", - "powerful", - "prelude", - "pretty", - "preview", - "principle", - "print", - "pro-", - "pro.", - "pro_", - "problem", - "proc", - "product", - "profile", - "profiler", - "program", - "progres", - "project", - "protocol", - "prototype", - "provider", - "proxy", - "public", - "pull", - "puppet", - "pure", - "purpose", - "push", - "pusher", - "pyramid", - "python", - "quality", - "query", - "queue", - "quick", - "rabbitmq", - "rack", - "radio", - "rail", - "railscast", - "random", - "range", - "raspberry", - "rdf-", - "rdf.", - "rdf_", - "react", - "reactive", - "read", - "reader", - "readme", - "ready", - "real", - "real-time", - "reality", - "realtime", - "recipe", - "recorder", - "red-", - "red.", - "red_", - "reddit", - "redi", - "redmine", - "reference", - "refinery", - "refresh", - "registry", - "related", - "release", - "remote", - "rendering", - "repo", - "report", - "request", - "require", - "required", - "requirej", - "research", - "resource", - "response", - "resque", - "rest", - "restful", - "resume", - "reveal", - "reverse", - "review", - "riak", - "rich", - "right", - "ring", - "robot", - "role", - "room", - "router", - "routing", - "rpc-", - "rpc.", - "rpc_", - "rpg-", - "rpg.", - "rpg_", - "rspec", - "ruby-", - "ruby.", - "ruby_", - "rule", - "run-", - "run.", - "run_", - "runner", - "running", - "runtime", - "rust", - "rvm-", - "rvm.", - "rvm_", - "salt", - "sample", - "sandbox", - "sas-", - "sas.", - "sas_", - "sbt-", - "sbt.", - "sbt_", - "scala", - "scalable", - "scanner", - "schema", - "scheme", - "school", - "science", - "scraper", - "scratch", - "screen", - "script", - "scroll", - "scs-", - "scs.", - "scs_", - "sdk-", - "sdk.", - "sdk_", - "sdl-", - "sdl.", - "sdl_", - "search", - "secure", - "security", - "see-", - "see.", - "see_", - "seed", - "select", - "selector", - "selenium", - "semantic", - "sencha", - "send", - "sentiment", - "serie", - "server", - "service", - "session", - "set-", - "set.", - "set_", - "setting", - "setup", - "sha1", - "sha2", - "sha256", - "share", - "shared", - "sharing", - "sheet", - "shell", - "shield", - "shipping", - "shop", - "shopify", - "shortener", - "should", - "show", - "showcase", - "side", - "silex", - "simple", - "simulator", - "single", - "site", - "skeleton", - "sketch", - "skin", - "slack", - "slide", - "slider", - "slim", - "small", - "smart", - "smtp", - "snake", - "snapshot", - "snippet", - "soap", - "social", - "socket", - "software", - "solarized", - "solr", - "solution", - "solver", - "some", - "soon", - "source", - "space", - "spark", - "spatial", - "spec", - "sphinx", - "spine", - "spotify", - "spree", - "spring", - "sprite", - "sql-", - "sql.", - "sql_", - "sqlite", - "ssh-", - "ssh.", - "ssh_", - "stack", - "staging", - "standard", - "stanford", - "start", - "started", - "starter", - "startup", - "stat", - "statamic", - "state", - "static", - "statistic", - "statsd", - "statu", - "steam", - "step", - "still", - "stm-", - "stm.", - "stm_", - "storage", - "store", - "storm", - "story", - "strategy", - "stream", - "streaming", - "string", - "stripe", - "structure", - "studio", - "study", - "stuff", - "style", - "sublime", - "sugar", - "suite", - "summary", - "super", - "support", - "supported", - "svg-", - "svg.", - "svg_", - "svn-", - "svn.", - "svn_", - "swagger", - "swift", - "switch", - "switcher", - "symfony", - "symphony", - "sync", - "synopsi", - "syntax", - "system", - "tab-", - "tab.", - "tab_", - "table", - "tag-", - "tag.", - "tag_", - "talk", - "target", - "task", - "tcp-", - "tcp.", - "tcp_", - "tdd-", - "tdd.", - "tdd_", - "team", - "tech", - "template", - "term", - "terminal", - "testing", - "tetri", - "text", - "textmate", - "theme", - "theory", - "three", - "thrift", - "time", - "timeline", - "timer", - "tiny", - "tinymce", - "tip-", - "tip.", - "tip_", - "title", - "todo", - "todomvc", - "token", - "tool", - "toolbox", - "toolkit", - "top-", - "top.", - "top_", - "tornado", - "touch", - "tower", - "tracker", - "tracking", - "traffic", - "training", - "transfer", - "translate", - "transport", - "tree", - "trello", - "try-", - "try.", - "try_", - "tumblr", - "tut-", - "tut.", - "tut_", - "tutorial", - "tweet", - "twig", - "twitter", - "type", - "typo", - "ubuntu", - "uiview", - "ultimate", - "under", - "unit", - "unity", - "universal", - "unix", - "update", - "updated", - "upgrade", - "upload", - "uploader", - "uri-", - "uri.", - "uri_", - "url-", - "url.", - "url_", - "usage", - "usb-", - "usb.", - "usb_", - "use-", - "use.", - "use_", - "used", - "useful", - "user", - "using", - "util", - "utilitie", - "utility", - "vagrant", - "validator", - "value", - "variou", - "varnish", - "version", - "via-", - "via.", - "via_", - "video", - "view", - "viewer", - "vim-", - "vim.", - "vim_", - "vimrc", - "virtual", - "vision", - "visual", - "vpn", - "want", - "warning", - "watch", - "watcher", - "wave", - "way-", - "way.", - "way_", - "weather", - "web-", - "web_", - "webapp", - "webgl", - "webhook", - "webkit", - "webrtc", - "website", - "websocket", - "welcome", - "what", - "what'", - "when", - "where", - "which", - "why-", - "why.", - "why_", - "widget", - "wifi", - "wiki", - "win-", - "win.", - "win_", - "window", - "wip-", - "wip.", - "wip_", - "within", - "without", - "wizard", - "word", - "wordpres", - "work", - "worker", - "workflow", - "working", - "workshop", - "world", - "wrapper", - "write", - "writer", - "writing", - "written", - "www-", - "www.", - "www_", - "xamarin", - "xcode", - "xml-", - "xml.", - "xml_", - "xmpp", - "xxxxxx", - "yahoo", - "yaml", - "yandex", - "yeoman", - "yet-", - "yet.", - "yet_", - "yii-", - "yii.", - "yii_", - "youtube", - "yui-", - "yui.", - "yui_", - "zend", - "zero", - "zip-", - "zip.", - "zip_", - "zsh-", - "zsh.", - "zsh_", -] -[[rules.allowlists]] -regexTarget = "line" -regexes = [ - '''--mount=type=secret,''', - '''import[ \t]+{[ \t\w,]+}[ \t]+from[ \t]+['"][^'"]+['"]''', -] -[[rules.allowlists]] -condition = "AND" -paths = [ - '''\.bb$''','''\.bbappend$''','''\.bbclass$''','''\.inc$''', -] -regexTarget = "line" -regexes = [ - '''LICENSE[^=]*=\s*"[^"]+''', - '''LIC_FILES_CHKSUM[^=]*=\s*"[^"]+''', - '''SRC[^=]*=\s*"[a-zA-Z0-9]+''', -] - -[[rules]] -id = "github-app-token" -description = "Identified a GitHub App Token, which may compromise GitHub application integrations and source code security." -regex = '''(?:ghu|ghs)_[0-9a-zA-Z]{36}''' -entropy = 3 -keywords = [ - "ghu_", - "ghs_", -] -[[rules.allowlists]] -paths = [ - '''(?:^|/)@octokit/auth-token/README\.md$''', -] - -[[rules]] -id = "github-fine-grained-pat" -description = "Found a GitHub Fine-Grained Personal Access Token, risking unauthorized repository access and code manipulation." -regex = '''github_pat_\w{82}''' -entropy = 3 -keywords = ["github_pat_"] - -[[rules]] -id = "github-oauth" -description = "Discovered a GitHub OAuth Access Token, posing a risk of compromised GitHub account integrations and data leaks." -regex = '''gho_[0-9a-zA-Z]{36}''' -entropy = 3 -keywords = ["gho_"] - -[[rules]] -id = "github-pat" -description = "Uncovered a GitHub Personal Access Token, potentially leading to unauthorized repository access and sensitive content exposure." -regex = '''ghp_[0-9a-zA-Z]{36}''' -entropy = 3 -keywords = ["ghp_"] -[[rules.allowlists]] -paths = [ - '''(?:^|/)@octokit/auth-token/README\.md$''', -] - -[[rules]] -id = "github-refresh-token" -description = "Detected a GitHub Refresh Token, which could allow prolonged unauthorized access to GitHub services." -regex = '''ghr_[0-9a-zA-Z]{36}''' -entropy = 3 -keywords = ["ghr_"] - -[[rules]] -id = "gitlab-cicd-job-token" -description = "Identified a GitLab CI/CD Job Token, potential access to projects and some APIs on behalf of a user while the CI job is running." -regex = '''glcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}''' -entropy = 3 -keywords = ["glcbt-"] - -[[rules]] -id = "gitlab-deploy-token" -description = "Identified a GitLab Deploy Token, risking access to repositories, packages and containers with write access." -regex = '''gldt-[0-9a-zA-Z_\-]{20}''' -entropy = 3 -keywords = ["gldt-"] - -[[rules]] -id = "gitlab-feature-flag-client-token" -description = "Identified a GitLab feature flag client token, risks exposing user lists and features flags used by an application." -regex = '''glffct-[0-9a-zA-Z_\-]{20}''' -entropy = 3 -keywords = ["glffct-"] - -[[rules]] -id = "gitlab-feed-token" -description = "Identified a GitLab feed token, risking exposure of user data." -regex = '''glft-[0-9a-zA-Z_\-]{20}''' -entropy = 3 -keywords = ["glft-"] - -[[rules]] -id = "gitlab-incoming-mail-token" -description = "Identified a GitLab incoming mail token, risking manipulation of data sent by mail." -regex = '''glimt-[0-9a-zA-Z_\-]{25}''' -entropy = 3 -keywords = ["glimt-"] - -[[rules]] -id = "gitlab-kubernetes-agent-token" -description = "Identified a GitLab Kubernetes Agent token, risking access to repos and registry of projects connected via agent." -regex = '''glagent-[0-9a-zA-Z_\-]{50}''' -entropy = 3 -keywords = ["glagent-"] - -[[rules]] -id = "gitlab-oauth-app-secret" -description = "Identified a GitLab OIDC Application Secret, risking access to apps using GitLab as authentication provider." -regex = '''gloas-[0-9a-zA-Z_\-]{64}''' -entropy = 3 -keywords = ["gloas-"] - -[[rules]] -id = "gitlab-pat" -description = "Identified a GitLab Personal Access Token, risking unauthorized access to GitLab repositories and codebase exposure." -regex = '''glpat-[\w-]{20}''' -entropy = 3 -keywords = ["glpat-"] - -[[rules]] -id = "gitlab-pat-routable" -description = "Identified a GitLab Personal Access Token (routable), risking unauthorized access to GitLab repositories and codebase exposure." -regex = '''\bglpat-[0-9a-zA-Z_-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b''' -entropy = 4 -keywords = ["glpat-"] - -[[rules]] -id = "gitlab-ptt" -description = "Found a GitLab Pipeline Trigger Token, potentially compromising continuous integration workflows and project security." -regex = '''glptt-[0-9a-f]{40}''' -entropy = 3 -keywords = ["glptt-"] - -[[rules]] -id = "gitlab-rrt" -description = "Discovered a GitLab Runner Registration Token, posing a risk to CI/CD pipeline integrity and unauthorized access." -regex = '''GR1348941[\w-]{20}''' -entropy = 3 -keywords = ["gr1348941"] - -[[rules]] -id = "gitlab-runner-authentication-token" -description = "Discovered a GitLab Runner Authentication Token, posing a risk to CI/CD pipeline integrity and unauthorized access." -regex = '''glrt-[0-9a-zA-Z_\-]{20}''' -entropy = 3 -keywords = ["glrt-"] - -[[rules]] -id = "gitlab-runner-authentication-token-routable" -description = "Discovered a GitLab Runner Authentication Token (Routable), posing a risk to CI/CD pipeline integrity and unauthorized access." -regex = '''\bglrt-t\d_[0-9a-zA-Z_\-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b''' -entropy = 4 -keywords = ["glrt-"] - -[[rules]] -id = "gitlab-scim-token" -description = "Discovered a GitLab SCIM Token, posing a risk to unauthorized access for a organization or instance." -regex = '''glsoat-[0-9a-zA-Z_\-]{20}''' -entropy = 3 -keywords = ["glsoat-"] - -[[rules]] -id = "gitlab-session-cookie" -description = "Discovered a GitLab Session Cookie, posing a risk to unauthorized access to a user account." -regex = '''_gitlab_session=[0-9a-z]{32}''' -entropy = 3 -keywords = ["_gitlab_session="] - -[[rules]] -id = "gitter-access-token" -description = "Uncovered a Gitter Access Token, which may lead to unauthorized access to chat and communication services." -regex = '''(?i)[\w.-]{0,50}?(?:gitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["gitter"] - -[[rules]] -id = "gocardless-api-token" -description = "Detected a GoCardless API token, potentially risking unauthorized direct debit payment operations and financial data exposure." -regex = '''(?i)[\w.-]{0,50}?(?:gocardless)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(live_(?i)[a-z0-9\-_=]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "live_", - "gocardless", -] - -[[rules]] -id = "grafana-api-key" -description = "Identified a Grafana API key, which could compromise monitoring dashboards and sensitive data analytics." -regex = '''(?i)\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["eyjrijoi"] - -[[rules]] -id = "grafana-cloud-api-token" -description = "Found a Grafana cloud API token, risking unauthorized access to cloud-based monitoring services and data exposure." -regex = '''(?i)\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["glc_"] - -[[rules]] -id = "grafana-service-account-token" -description = "Discovered a Grafana service account token, posing a risk of compromised monitoring services and data integrity." -regex = '''(?i)\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["glsa_"] - -[[rules]] -id = "harness-api-key" -description = "Identified a Harness Access Token (PAT or SAT), risking unauthorized access to a Harness account." -regex = '''(?:pat|sat)\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9]{24}\.[a-zA-Z0-9]{20}''' -keywords = [ - "pat.", - "sat.", -] - -[[rules]] -id = "hashicorp-tf-api-token" -description = "Uncovered a HashiCorp Terraform user/org API token, which may lead to unauthorized infrastructure management and security breaches." -regex = '''(?i)[a-z0-9]{14}\.(?-i:atlasv1)\.[a-z0-9\-_=]{60,70}''' -entropy = 3.5 -keywords = ["atlasv1"] - -[[rules]] -id = "hashicorp-tf-password" -description = "Identified a HashiCorp Terraform password field, risking unauthorized infrastructure configuration and security breaches." -regex = '''(?i)[\w.-]{0,50}?(?:administrator_login_password|password)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}("[a-z0-9=_\-]{8,20}")(?:[\x60'"\s;]|\\[nr]|$)''' -path = '''(?i)\.(?:tf|hcl)$''' -entropy = 2 -keywords = [ - "administrator_login_password", - "password", -] - -[[rules]] -id = "heroku-api-key" -description = "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security." -regex = '''(?i)[\w.-]{0,50}?(?:heroku)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["heroku"] - -[[rules]] -id = "hubspot-api-key" -description = "Found a HubSpot API Token, posing a risk to CRM data integrity and unauthorized marketing operations." -regex = '''(?i)[\w.-]{0,50}?(?:hubspot)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["hubspot"] - -[[rules]] -id = "huggingface-access-token" -description = "Discovered a Hugging Face Access token, which could lead to unauthorized access to AI models and sensitive data." -regex = '''\b(hf_(?i:[a-z]{34}))(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["hf_"] - -[[rules]] -id = "huggingface-organization-api-token" -description = "Uncovered a Hugging Face Organization API token, potentially compromising AI organization accounts and associated data." -regex = '''\b(api_org_(?i:[a-z]{34}))(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["api_org_"] - -[[rules]] -id = "infracost-api-token" -description = "Detected an Infracost API Token, risking unauthorized access to cloud cost estimation tools and financial data." -regex = '''\b(ico-[a-zA-Z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["ico-"] - -[[rules]] -id = "intercom-api-key" -description = "Identified an Intercom API Token, which could compromise customer communication channels and data privacy." -regex = '''(?i)[\w.-]{0,50}?(?:intercom)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{60})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["intercom"] - -[[rules]] -id = "intra42-client-secret" -description = "Found a Intra42 client secret, which could lead to unauthorized access to the 42School API and sensitive data." -regex = '''\b(s-s4t2(?:ud|af)-(?i)[abcdef0123456789]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = [ - "intra", - "s-s4t2ud-", - "s-s4t2af-", -] - -[[rules]] -id = "jfrog-api-key" -description = "Found a JFrog API Key, posing a risk of unauthorized access to software artifact repositories and build pipelines." -regex = '''(?i)[\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{73})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "jfrog", - "artifactory", - "bintray", - "xray", -] - -[[rules]] -id = "jfrog-identity-token" -description = "Discovered a JFrog Identity Token, potentially compromising access to JFrog services and sensitive software artifacts." -regex = '''(?i)[\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "jfrog", - "artifactory", - "bintray", - "xray", -] - -[[rules]] -id = "jwt" -description = "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data." -regex = '''\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?)(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["ey"] - -[[rules]] -id = "jwt-base64" -description = "Detected a Base64-encoded JSON Web Token, posing a risk of exposing encoded authentication and data exchange information." -regex = '''\bZXlK(?:(?PaGJHY2lPaU)|(?PaGNIVWlPaU)|(?PaGNIWWlPaU)|(?PaGRXUWlPaU)|(?PaU5qUWlP)|(?PamNtbDBJanBi)|(?PamRIa2lPaU)|(?PbGNHc2lPbn)|(?PbGJtTWlPaU)|(?PcWEzVWlPaU)|(?PcWQyc2lPb)|(?PcGMzTWlPaU)|(?PcGRpSTZJ)|(?PcmFXUWlP)|(?PclpYbGZiM0J6SWpwY)|(?PcmRIa2lPaUp)|(?PdWIyNWpaU0k2)|(?Pd01tTWlP)|(?Pd01uTWlPaU)|(?Pd2NIUWlPaU)|(?PemRXSWlPaU)|(?PemRuUWlP)|(?PMFlXY2lPaU)|(?PMGVYQWlPaUp)|(?PMWNtd2l)|(?PMWMyVWlPaUp)|(?PMlpYSWlPaU)|(?PMlpYSnphVzl1SWpv)|(?PNElqb2)|(?PNE5XTWlP)|(?PNE5YUWlPaU)|(?PNE5YUWpVekkxTmlJNkl)|(?PNE5YVWlPaU)|(?PNmFYQWlPaU))[a-zA-Z0-9\/\\_+\-\r\n]{40,}={0,2}''' -entropy = 2 -keywords = ["zxlk"] - -[[rules]] -id = "kraken-access-token" -description = "Identified a Kraken Access Token, potentially compromising cryptocurrency trading accounts and financial security." -regex = '''(?i)[\w.-]{0,50}?(?:kraken)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9\/=_\+\-]{80,90})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["kraken"] - -[[rules]] -id = "kubernetes-secret-yaml" -description = "Possible Kubernetes Secret detected, posing a risk of leaking credentials/tokens from your deployments" -regex = '''(?i)(?:\bkind:[ \t]*["']?\bsecret\b["']?(?s:.){0,200}?\bdata:(?s:.){0,100}?\s+([\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:["']?[a-z0-9+/]{10,}={0,3}["']?|\{\{[ \t\w"|$:=,.-]+}}|""|''))|\bdata:(?s:.){0,100}?\s+([\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:["']?[a-z0-9+/]{10,}={0,3}["']?|\{\{[ \t\w"|$:=,.-]+}}|""|''))(?s:.){0,200}?\bkind:[ \t]*["']?\bsecret\b["']?)''' -path = '''(?i)\.ya?ml$''' -keywords = ["secret"] -[[rules.allowlists]] -regexes = [ - '''[\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:\{\{[ \t\w"|$:=,.-]+}}|""|'')''', -] -[[rules.allowlists]] -regexTarget = "match" -regexes = [ - '''(kind:(?s:.)+\n---\n(?s:.)+\bdata:|data:(?s:.)+\n---\n(?s:.)+\bkind:)''', -] - -[[rules]] -id = "kucoin-access-token" -description = "Found a Kucoin Access Token, risking unauthorized access to cryptocurrency exchange services and transactions." -regex = '''(?i)[\w.-]{0,50}?(?:kucoin)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["kucoin"] - -[[rules]] -id = "kucoin-secret-key" -description = "Discovered a Kucoin Secret Key, which could lead to compromised cryptocurrency operations and financial data breaches." -regex = '''(?i)[\w.-]{0,50}?(?:kucoin)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["kucoin"] - -[[rules]] -id = "launchdarkly-access-token" -description = "Uncovered a Launchdarkly Access Token, potentially compromising feature flag management and application functionality." -regex = '''(?i)[\w.-]{0,50}?(?:launchdarkly)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["launchdarkly"] - -[[rules]] -id = "linear-api-key" -description = "Detected a Linear API Token, posing a risk to project management tools and sensitive task data." -regex = '''lin_api_(?i)[a-z0-9]{40}''' -entropy = 2 -keywords = ["lin_api_"] - -[[rules]] -id = "linear-client-secret" -description = "Identified a Linear Client Secret, which may compromise secure integrations and sensitive project management data." -regex = '''(?i)[\w.-]{0,50}?(?:linear)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["linear"] - -[[rules]] -id = "linkedin-client-id" -description = "Found a LinkedIn Client ID, risking unauthorized access to LinkedIn integrations and professional data exposure." -regex = '''(?i)[\w.-]{0,50}?(?:linked[_-]?in)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{14})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = [ - "linkedin", - "linked_in", - "linked-in", -] - -[[rules]] -id = "linkedin-client-secret" -description = "Discovered a LinkedIn Client secret, potentially compromising LinkedIn application integrations and user data." -regex = '''(?i)[\w.-]{0,50}?(?:linked[_-]?in)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = [ - "linkedin", - "linked_in", - "linked-in", -] - -[[rules]] -id = "lob-api-key" -description = "Uncovered a Lob API Key, which could lead to unauthorized access to mailing and address verification services." -regex = '''(?i)[\w.-]{0,50}?(?:lob)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}((live|test)_[a-f0-9]{35})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "test_", - "live_", -] - -[[rules]] -id = "lob-pub-api-key" -description = "Detected a Lob Publishable API Key, posing a risk of exposing mail and print service integrations." -regex = '''(?i)[\w.-]{0,50}?(?:lob)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}((test|live)_pub_[a-f0-9]{31})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "test_pub", - "live_pub", - "_pub", -] - -[[rules]] -id = "mailchimp-api-key" -description = "Identified a Mailchimp API key, potentially compromising email marketing campaigns and subscriber data." -regex = '''(?i)[\w.-]{0,50}?(?:MailchimpSDK.initialize|mailchimp)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32}-us\d\d)(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["mailchimp"] - -[[rules]] -id = "mailgun-private-api-token" -description = "Found a Mailgun private API token, risking unauthorized email service operations and data breaches." -regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(key-[a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["mailgun"] - -[[rules]] -id = "mailgun-pub-key" -description = "Discovered a Mailgun public validation key, which could expose email verification processes and associated data." -regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(pubkey-[a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["mailgun"] - -[[rules]] -id = "mailgun-signing-key" -description = "Uncovered a Mailgun webhook signing key, potentially compromising email automation and data integrity." -regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["mailgun"] - -[[rules]] -id = "mapbox-api-token" -description = "Detected a MapBox API token, posing a risk to geospatial services and sensitive location data exposure." -regex = '''(?i)[\w.-]{0,50}?(?:mapbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(pk\.[a-z0-9]{60}\.[a-z0-9]{22})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["mapbox"] - -[[rules]] -id = "mattermost-access-token" -description = "Identified a Mattermost Access Token, which may compromise team communication channels and data privacy." -regex = '''(?i)[\w.-]{0,50}?(?:mattermost)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{26})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["mattermost"] - -[[rules]] -id = "maxmind-license-key" -description = "Discovered a potential MaxMind license key." -regex = '''\b([A-Za-z0-9]{6}_[A-Za-z0-9]{29}_mmk)(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 4 -keywords = ["_mmk"] - -[[rules]] -id = "messagebird-api-token" -description = "Found a MessageBird API token, risking unauthorized access to communication platforms and message data." -regex = '''(?i)[\w.-]{0,50}?(?:message[_-]?bird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{25})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "messagebird", - "message-bird", - "message_bird", -] - -[[rules]] -id = "messagebird-client-id" -description = "Discovered a MessageBird client ID, potentially compromising API integrations and sensitive communication data." -regex = '''(?i)[\w.-]{0,50}?(?:message[_-]?bird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "messagebird", - "message-bird", - "message_bird", -] - -[[rules]] -id = "microsoft-teams-webhook" -description = "Uncovered a Microsoft Teams Webhook, which could lead to unauthorized access to team collaboration tools and data leaks." -regex = '''https://[a-z0-9]+\.webhook\.office\.com/webhookb2/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/IncomingWebhook/[a-z0-9]{32}/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}''' -keywords = [ - "webhook.office.com", - "webhookb2", - "incomingwebhook", -] - -[[rules]] -id = "netlify-access-token" -description = "Detected a Netlify Access Token, potentially compromising web hosting services and site management." -regex = '''(?i)[\w.-]{0,50}?(?:netlify)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{40,46})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["netlify"] - -[[rules]] -id = "new-relic-browser-api-token" -description = "Identified a New Relic ingest browser API token, risking unauthorized access to application performance data and analytics." -regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRJS-[a-f0-9]{19})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["nrjs-"] - -[[rules]] -id = "new-relic-insert-key" -description = "Discovered a New Relic insight insert key, compromising data injection into the platform." -regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRII-[a-z0-9-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["nrii-"] - -[[rules]] -id = "new-relic-user-api-id" -description = "Found a New Relic user API ID, posing a risk to application monitoring services and data integrity." -regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "new-relic", - "newrelic", - "new_relic", -] - -[[rules]] -id = "new-relic-user-api-key" -description = "Discovered a New Relic user API Key, which could lead to compromised application insights and performance monitoring." -regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRAK-[a-z0-9]{27})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["nrak"] - -[[rules]] -id = "npm-access-token" -description = "Uncovered an npm access token, potentially compromising package management and code repository access." -regex = '''(?i)\b(npm_[a-z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["npm_"] - -[[rules]] -id = "nuget-config-password" -description = "Identified a password within a Nuget config file, potentially compromising package management access." -regex = '''(?i)''' -path = '''(?i)nuget\.config$''' -entropy = 1 -keywords = ["|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "nytimes", - "new-york-times", - "newyorktimes", -] - -[[rules]] -id = "octopus-deploy-api-key" -description = "Discovered a potential Octopus Deploy API key, risking application deployments and operational security." -regex = '''\b(API-[A-Z0-9]{26})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["api-"] - -[[rules]] -id = "okta-access-token" -description = "Identified an Okta Access Token, which may compromise identity management services and user authentication data." -regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Oo]kta|OKTA))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(00[\w=\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 4 -keywords = ["okta"] - -[[rules]] -id = "openai-api-key" -description = "Found an OpenAI API Key, posing a risk of unauthorized access to AI services and data manipulation." -regex = '''\b(sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["t3blbkfj"] - -[[rules]] -id = "openshift-user-token" -description = "Found an OpenShift user token, potentially compromising an OpenShift/Kubernetes cluster." -regex = '''\b(sha256~[\w-]{43})(?:[^\w-]|\z)''' -entropy = 3.5 -keywords = ["sha256~"] - -[[rules]] -id = "perplexity-api-key" -description = "Detected a Perplexity API key, which could lead to unauthorized access to Perplexity AI services and data exposure." -regex = '''\b(pplx-[a-zA-Z0-9]{48})(?:[\x60'"\s;]|\\[nr]|$|\b)''' -entropy = 4 -keywords = ["pplx-"] - -[[rules]] -id = "pkcs12-file" -description = "Found a PKCS #12 file, which commonly contain bundled private keys." -path = '''(?i)(?:^|\/)[^\/]+\.p(?:12|fx)$''' - -[[rules]] -id = "plaid-api-token" -description = "Discovered a Plaid API Token, potentially compromising financial data aggregation and banking services." -regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(access-(?:sandbox|development|production)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["plaid"] - -[[rules]] -id = "plaid-client-id" -description = "Uncovered a Plaid Client ID, which could lead to unauthorized financial service integrations and data breaches." -regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3.5 -keywords = ["plaid"] - -[[rules]] -id = "plaid-secret-key" -description = "Detected a Plaid Secret key, risking unauthorized access to financial accounts and sensitive transaction data." -regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3.5 -keywords = ["plaid"] - -[[rules]] -id = "planetscale-api-token" -description = "Identified a PlanetScale API token, potentially compromising database management and operations." -regex = '''\b(pscale_tkn_(?i)[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["pscale_tkn_"] - -[[rules]] -id = "planetscale-oauth-token" -description = "Found a PlanetScale OAuth token, posing a risk to database access control and sensitive data integrity." -regex = '''\b(pscale_oauth_[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["pscale_oauth_"] - -[[rules]] -id = "planetscale-password" -description = "Discovered a PlanetScale password, which could lead to unauthorized database operations and data breaches." -regex = '''(?i)\b(pscale_pw_(?i)[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["pscale_pw_"] - -[[rules]] -id = "postman-api-token" -description = "Uncovered a Postman API token, potentially compromising API testing and development workflows." -regex = '''\b(PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["pmak-"] - -[[rules]] -id = "prefect-api-token" -description = "Detected a Prefect API token, risking unauthorized access to workflow management and automation services." -regex = '''\b(pnu_[a-zA-Z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["pnu_"] - -[[rules]] -id = "private-key" -description = "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption." -regex = '''(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]{64,}?KEY(?: BLOCK)?-----''' -keywords = ["-----begin"] - -[[rules]] -id = "privateai-api-token" -description = "Identified a PrivateAI Token, posing a risk of unauthorized access to AI services and data manipulation." -regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:private[_-]?ai)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = [ - "privateai", - "private_ai", - "private-ai", -] - -[[rules]] -id = "pulumi-api-token" -description = "Found a Pulumi API token, posing a risk to infrastructure as code services and cloud resource management." -regex = '''\b(pul-[a-f0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["pul-"] - -[[rules]] -id = "pypi-upload-token" -description = "Discovered a PyPI upload token, potentially compromising Python package distribution and repository integrity." -regex = '''pypi-AgEIcHlwaS5vcmc[\w-]{50,1000}''' -entropy = 3 -keywords = ["pypi-ageichlwas5vcmc"] - -[[rules]] -id = "rapidapi-access-token" -description = "Uncovered a RapidAPI Access Token, which could lead to unauthorized access to various APIs and data services." -regex = '''(?i)[\w.-]{0,50}?(?:rapidapi)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{50})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["rapidapi"] - -[[rules]] -id = "readme-api-token" -description = "Detected a Readme API token, risking unauthorized documentation management and content exposure." -regex = '''\b(rdme_[a-z0-9]{70})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["rdme_"] - -[[rules]] -id = "rubygems-api-token" -description = "Identified a Rubygem API token, potentially compromising Ruby library distribution and package management." -regex = '''\b(rubygems_[a-f0-9]{48})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["rubygems_"] - -[[rules]] -id = "scalingo-api-token" -description = "Found a Scalingo API token, posing a risk to cloud platform services and application deployment security." -regex = '''\b(tk-us-[\w-]{48})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["tk-us-"] - -[[rules]] -id = "sendbird-access-id" -description = "Discovered a Sendbird Access ID, which could compromise chat and messaging platform integrations." -regex = '''(?i)[\w.-]{0,50}?(?:sendbird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["sendbird"] - -[[rules]] -id = "sendbird-access-token" -description = "Uncovered a Sendbird Access Token, potentially risking unauthorized access to communication services and user data." -regex = '''(?i)[\w.-]{0,50}?(?:sendbird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["sendbird"] - -[[rules]] -id = "sendgrid-api-token" -description = "Detected a SendGrid API token, posing a risk of unauthorized email service operations and data exposure." -regex = '''\b(SG\.(?i)[a-z0-9=_\-\.]{66})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["sg."] - -[[rules]] -id = "sendinblue-api-token" -description = "Identified a Sendinblue API token, which may compromise email marketing services and subscriber data privacy." -regex = '''\b(xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["xkeysib-"] - -[[rules]] -id = "sentry-access-token" -description = "Found a Sentry.io Access Token (old format), risking unauthorized access to error tracking services and sensitive application data." -regex = '''(?i)[\w.-]{0,50}?(?:sentry)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["sentry"] - -[[rules]] -id = "sentry-org-token" -description = "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data." -regex = '''\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|\z)''' -entropy = 4.5 -keywords = ["sntrys_eyjpyxqio"] - -[[rules]] -id = "sentry-user-token" -description = "Found a Sentry.io User Token, risking unauthorized access to error tracking services and sensitive application data." -regex = '''\b(sntryu_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3.5 -keywords = ["sntryu_"] - -[[rules]] -id = "settlemint-application-access-token" -description = "Found a Settlemint Application Access Token." -regex = '''\b(sm_aat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["sm_aat"] - -[[rules]] -id = "settlemint-personal-access-token" -description = "Found a Settlemint Personal Access Token." -regex = '''\b(sm_pat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["sm_pat"] - -[[rules]] -id = "settlemint-service-access-token" -description = "Found a Settlemint Service Access Token." -regex = '''\b(sm_sat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["sm_sat"] - -[[rules]] -id = "shippo-api-token" -description = "Discovered a Shippo API token, potentially compromising shipping services and customer order data." -regex = '''\b(shippo_(?:live|test)_[a-fA-F0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = ["shippo_"] - -[[rules]] -id = "shopify-access-token" -description = "Uncovered a Shopify access token, which could lead to unauthorized e-commerce platform access and data breaches." -regex = '''shpat_[a-fA-F0-9]{32}''' -entropy = 2 -keywords = ["shpat_"] - -[[rules]] -id = "shopify-custom-access-token" -description = "Detected a Shopify custom access token, potentially compromising custom app integrations and e-commerce data security." -regex = '''shpca_[a-fA-F0-9]{32}''' -entropy = 2 -keywords = ["shpca_"] - -[[rules]] -id = "shopify-private-app-access-token" -description = "Identified a Shopify private app access token, risking unauthorized access to private app data and store operations." -regex = '''shppa_[a-fA-F0-9]{32}''' -entropy = 2 -keywords = ["shppa_"] - -[[rules]] -id = "shopify-shared-secret" -description = "Found a Shopify shared secret, posing a risk to application authentication and e-commerce platform security." -regex = '''shpss_[a-fA-F0-9]{32}''' -entropy = 2 -keywords = ["shpss_"] - -[[rules]] -id = "sidekiq-secret" -description = "Discovered a Sidekiq Secret, which could lead to compromised background job processing and application data breaches." -regex = '''(?i)[\w.-]{0,50}?(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = [ - "bundle_enterprise__contribsys__com", - "bundle_gems__contribsys__com", -] - -[[rules]] -id = "sidekiq-sensitive-url" -description = "Uncovered a Sidekiq Sensitive URL, potentially exposing internal job queues and sensitive operation details." -regex = '''(?i)\bhttps?://([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)''' -keywords = [ - "gems.contribsys.com", - "enterprise.contribsys.com", -] - -[[rules]] -id = "slack-app-token" -description = "Detected a Slack App-level token, risking unauthorized access to Slack applications and workspace data." -regex = '''(?i)xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+''' -entropy = 2 -keywords = ["xapp"] - -[[rules]] -id = "slack-bot-token" -description = "Identified a Slack Bot token, which may compromise bot integrations and communication channel security." -regex = '''xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*''' -entropy = 3 -keywords = ["xoxb"] - -[[rules]] -id = "slack-config-access-token" -description = "Found a Slack Configuration access token, posing a risk to workspace configuration and sensitive data access." -regex = '''(?i)xoxe.xox[bp]-\d-[A-Z0-9]{163,166}''' -entropy = 2 -keywords = [ - "xoxe.xoxb-", - "xoxe.xoxp-", -] - -[[rules]] -id = "slack-config-refresh-token" -description = "Discovered a Slack Configuration refresh token, potentially allowing prolonged unauthorized access to configuration settings." -regex = '''(?i)xoxe-\d-[A-Z0-9]{146}''' -entropy = 2 -keywords = ["xoxe-"] - -[[rules]] -id = "slack-legacy-bot-token" -description = "Uncovered a Slack Legacy bot token, which could lead to compromised legacy bot operations and data exposure." -regex = '''xoxb-[0-9]{8,14}-[a-zA-Z0-9]{18,26}''' -entropy = 2 -keywords = ["xoxb"] - -[[rules]] -id = "slack-legacy-token" -description = "Detected a Slack Legacy token, risking unauthorized access to older Slack integrations and user data." -regex = '''xox[os]-\d+-\d+-\d+-[a-fA-F\d]+''' -entropy = 2 -keywords = [ - "xoxo", - "xoxs", -] - -[[rules]] -id = "slack-legacy-workspace-token" -description = "Identified a Slack Legacy Workspace token, potentially compromising access to workspace data and legacy features." -regex = '''xox[ar]-(?:\d-)?[0-9a-zA-Z]{8,48}''' -entropy = 2 -keywords = [ - "xoxa", - "xoxr", -] - -[[rules]] -id = "slack-user-token" -description = "Found a Slack User token, posing a risk of unauthorized user impersonation and data access within Slack workspaces." -regex = '''xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34}''' -entropy = 2 -keywords = [ - "xoxp-", - "xoxe-", -] - -[[rules]] -id = "slack-webhook-url" -description = "Discovered a Slack Webhook, which could lead to unauthorized message posting and data leakage in Slack channels." -regex = '''(?:https?://)?hooks.slack.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{43,56}''' -keywords = ["hooks.slack.com"] - -[[rules]] -id = "snyk-api-token" -description = "Uncovered a Snyk API token, potentially compromising software vulnerability scanning and code security." -regex = '''(?i)[\w.-]{0,50}?(?:snyk[_.-]?(?:(?:api|oauth)[_.-]?)?(?:key|token))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["snyk"] - -[[rules]] -id = "sonar-api-token" -description = "Uncovered a Sonar API token, potentially compromising software vulnerability scanning and code security." -regex = '''(?i)[\w.-]{0,50}?(?:sonar[_.-]?(login|token))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["sonar"] - -[[rules]] -id = "sourcegraph-access-token" -description = "Sourcegraph is a code search and navigation engine." -regex = '''(?i)\b(\b(sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40}|[a-fA-F0-9]{40})\b)(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = [ - "sgp_", - "sourcegraph", -] - -[[rules]] -id = "square-access-token" -description = "Detected a Square Access Token, risking unauthorized payment processing and financial transaction exposure." -regex = '''\b((?:EAAA|sq0atp-)[\w-]{22,60})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = [ - "sq0atp-", - "eaaa", -] - -[[rules]] -id = "squarespace-access-token" -description = "Identified a Squarespace Access Token, which may compromise website management and content control on Squarespace." -regex = '''(?i)[\w.-]{0,50}?(?:squarespace)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["squarespace"] - -[[rules]] -id = "stripe-access-token" -description = "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data." -regex = '''\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 2 -keywords = [ - "sk_test", - "sk_live", - "sk_prod", - "rk_test", - "rk_live", - "rk_prod", -] - -[[rules]] -id = "sumologic-access-id" -description = "Discovered a SumoLogic Access ID, potentially compromising log management services and data analytics integrity." -regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(su[a-zA-Z0-9]{12})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["sumo"] - -[[rules]] -id = "sumologic-access-token" -description = "Uncovered a SumoLogic Access Token, which could lead to unauthorized access to log data and analytics insights." -regex = '''(?i)[\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3 -keywords = ["sumo"] - -[[rules]] -id = "telegram-bot-api-token" -description = "Detected a Telegram Bot API Token, risking unauthorized bot operations and message interception on Telegram." -regex = '''(?i)[\w.-]{0,50}?(?:telegr)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{5,16}:(?-i:A)[a-z0-9_\-]{34})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["telegr"] - -[[rules]] -id = "travisci-access-token" -description = "Identified a Travis CI Access Token, potentially compromising continuous integration services and codebase security." -regex = '''(?i)[\w.-]{0,50}?(?:travis)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{22})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["travis"] - -[[rules]] -id = "twilio-api-key" -description = "Found a Twilio API Key, posing a risk to communication services and sensitive customer interaction data." -regex = '''SK[0-9a-fA-F]{32}''' -entropy = 3 -keywords = ["sk"] - -[[rules]] -id = "twitch-api-token" -description = "Discovered a Twitch API token, which could compromise streaming services and account integrations." -regex = '''(?i)[\w.-]{0,50}?(?:twitch)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["twitch"] - -[[rules]] -id = "twitter-access-secret" -description = "Uncovered a Twitter Access Secret, potentially risking unauthorized Twitter integrations and data breaches." -regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{45})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["twitter"] - -[[rules]] -id = "twitter-access-token" -description = "Detected a Twitter Access Token, posing a risk of unauthorized account operations and social media data exposure." -regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{15,25}-[a-zA-Z0-9]{20,40})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["twitter"] - -[[rules]] -id = "twitter-api-key" -description = "Identified a Twitter API Key, which may compromise Twitter application integrations and user data security." -regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{25})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["twitter"] - -[[rules]] -id = "twitter-api-secret" -description = "Found a Twitter API Secret, risking the security of Twitter app integrations and sensitive data access." -regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{50})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["twitter"] - -[[rules]] -id = "twitter-bearer-token" -description = "Discovered a Twitter Bearer Token, potentially compromising API access and data retrieval from Twitter." -regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(A{22}[a-zA-Z0-9%]{80,100})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["twitter"] - -[[rules]] -id = "typeform-api-token" -description = "Uncovered a Typeform API token, which could lead to unauthorized survey management and data collection." -regex = '''(?i)[\w.-]{0,50}?(?:typeform)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(tfp_[a-z0-9\-_\.=]{59})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["tfp_"] - -[[rules]] -id = "vault-batch-token" -description = "Detected a Vault Batch Token, risking unauthorized access to secret management services and sensitive data." -regex = '''\b(hvb\.[\w-]{138,300})(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 4 -keywords = ["hvb."] - -[[rules]] -id = "vault-service-token" -description = "Identified a Vault Service Token, potentially compromising infrastructure security and access to sensitive credentials." -regex = '''\b((?:hvs\.[\w-]{90,120}|s\.(?i:[a-z0-9]{24})))(?:[\x60'"\s;]|\\[nr]|$)''' -entropy = 3.5 -keywords = [ - "hvs.", - "s.", -] -[[rules.allowlists]] -regexes = [ - '''s\.[A-Za-z]{24}''', -] - -[[rules]] -id = "yandex-access-token" -description = "Found a Yandex Access Token, posing a risk to Yandex service integrations and user data privacy." -regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(t1\.[A-Z0-9a-z_-]+[=]{0,2}\.[A-Z0-9a-z_-]{86}[=]{0,2})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["yandex"] - -[[rules]] -id = "yandex-api-key" -description = "Discovered a Yandex API Key, which could lead to unauthorized access to Yandex services and data manipulation." -regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(AQVN[A-Za-z0-9_\-]{35,38})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["yandex"] - -[[rules]] -id = "yandex-aws-access-token" -description = "Uncovered a Yandex AWS Access Token, potentially compromising cloud resource access and data security on Yandex Cloud." -regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(YC[a-zA-Z0-9_\-]{38})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["yandex"] - -[[rules]] -id = "zendesk-secret-key" -description = "Detected a Zendesk Secret Key, risking unauthorized access to customer support services and sensitive ticketing data." -regex = '''(?i)[\w.-]{0,50}?(?:zendesk)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' -keywords = ["zendesk"] - diff --git a/cli/detect/config/rule.go b/cli/detect/config/rule.go deleted file mode 100644 index 6d2b61326..000000000 --- a/cli/detect/config/rule.go +++ /dev/null @@ -1,114 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package config - -import ( - "fmt" - "strings" - - "github.com/Infisical/infisical-merge/detect/regexp" -) - -// Rules contain information that define details on how to detect secrets -type Rule struct { - // RuleID is a unique identifier for this rule - RuleID string - - // Description is the description of the rule. - Description string - - // Entropy is a float representing the minimum shannon - // entropy a regex group must have to be considered a secret. - Entropy float64 - - // SecretGroup is an int used to extract secret from regex - // match and used as the group that will have its entropy - // checked if `entropy` is set. - SecretGroup int - - // Regex is a golang regular expression used to detect secrets. - Regex *regexp.Regexp - - // Path is a golang regular expression used to - // filter secrets by path - Path *regexp.Regexp - - // Tags is an array of strings used for metadata - // and reporting purposes. - Tags []string - - // Keywords are used for pre-regex check filtering. Rules that contain - // keywords will perform a quick string compare check to make sure the - // keyword(s) are in the content being scanned. - Keywords []string - - // Allowlists allows a rule to be ignored for specific commits, paths, regexes, and/or stopwords. - Allowlists []*Allowlist - - // validated is an internal flag to track whether `Validate()` has been called. - validated bool -} - -// Validate guards against common misconfigurations. -func (r *Rule) Validate() error { - if r.validated { - return nil - } - - // Ensure |id| is present. - if strings.TrimSpace(r.RuleID) == "" { - // Try to provide helpful context, since |id| is empty. - var context string - if r.Regex != nil { - context = ", regex: " + r.Regex.String() - } else if r.Path != nil { - context = ", path: " + r.Path.String() - } else if r.Description != "" { - context = ", description: " + r.Description - } - return fmt.Errorf("rule |id| is missing or empty" + context) - } - - // Ensure the rule actually matches something. - if r.Regex == nil && r.Path == nil { - return fmt.Errorf("%s: both |regex| and |path| are empty, this rule will have no effect", r.RuleID) - } - - // Ensure |secretGroup| works. - if r.Regex != nil && r.SecretGroup > r.Regex.NumSubexp() { - return fmt.Errorf("%s: invalid regex secret group %d, max regex secret group %d", r.RuleID, r.SecretGroup, r.Regex.NumSubexp()) - } - - for _, allowlist := range r.Allowlists { - // This will probably never happen. - if allowlist == nil { - continue - } - if err := allowlist.Validate(); err != nil { - return fmt.Errorf("%s: %w", r.RuleID, err) - } - } - - r.validated = true - return nil -} diff --git a/cli/detect/config/utils.go b/cli/detect/config/utils.go deleted file mode 100644 index e28a5cb37..000000000 --- a/cli/detect/config/utils.go +++ /dev/null @@ -1,46 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package config - -import ( - "github.com/Infisical/infisical-merge/detect/regexp" -) - -func anyRegexMatch(f string, res []*regexp.Regexp) bool { - for _, re := range res { - if regexMatched(f, re) { - return true - } - } - return false -} - -func regexMatched(f string, re *regexp.Regexp) bool { - if re == nil { - return false - } - if re.FindString(f) != "" { - return true - } - return false -} diff --git a/cli/detect/decoder.go b/cli/detect/decoder.go deleted file mode 100644 index 6ec509757..000000000 --- a/cli/detect/decoder.go +++ /dev/null @@ -1,328 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package detect - -import ( - "bytes" - "encoding/base64" - "fmt" - "regexp" - "unicode" - - "github.com/Infisical/infisical-merge/detect/logging" -) - -var b64LikelyChars [128]byte -var b64Regexp = regexp.MustCompile(`[\w/+-]{16,}={0,3}`) -var decoders = []func(string) ([]byte, error){ - base64.StdEncoding.DecodeString, - base64.RawURLEncoding.DecodeString, -} - -func init() { - // Basically look for anything that isn't just letters - for _, c := range `0123456789+/-_` { - b64LikelyChars[c] = 1 - } -} - -// EncodedSegment represents a portion of text that is encoded in some way. -// `decode` supports recusive decoding and can result in "segment trees". -// There can be multiple segments in the original text, so each can be thought -// of as its own tree with the root being the original segment. -type EncodedSegment struct { - // The parent segment in a segment tree. If nil, it is a root segment - parent *EncodedSegment - - // Relative start/end are the bounds of the encoded value in the current pass. - relativeStart int - relativeEnd int - - // Absolute start/end refer to the bounds of the root segment in this segment - // tree - absoluteStart int - absoluteEnd int - - // Decoded start/end refer to the bounds of the decoded value in the current - // pass. These can differ from relative values because decoding can shrink - // or grow the size of the segment. - decodedStart int - decodedEnd int - - // This is the actual decoded content in the segment - decodedValue string - - // This is the type of encoding - encoding string -} - -// isChildOf inspects the bounds of two segments to determine -// if one should be the child of another -func (s EncodedSegment) isChildOf(parent EncodedSegment) bool { - return parent.decodedStart <= s.relativeStart && parent.decodedEnd >= s.relativeEnd -} - -// decodedOverlaps checks if the decoded bounds of the segment overlaps a range -func (s EncodedSegment) decodedOverlaps(start, end int) bool { - return start <= s.decodedEnd && end >= s.decodedStart -} - -// adjustMatchIndex takes the matchIndex from the current decoding pass and -// updates it to match the absolute matchIndex in the original text. -func (s EncodedSegment) adjustMatchIndex(matchIndex []int) []int { - // The match is within the bounds of the segment so we just return - // the absolute start and end of the root segment. - if s.decodedStart <= matchIndex[0] && matchIndex[1] <= s.decodedEnd { - return []int{ - s.absoluteStart, - s.absoluteEnd, - } - } - - // Since it overlaps one side and/or the other, we're going to have to adjust - // and climb parents until we're either at the root or we've determined - // we're fully inside one of the parent segments. - adjustedMatchIndex := make([]int, 2) - - if matchIndex[0] < s.decodedStart { - // It starts before the encoded segment so adjust the start to match - // the location before it was decoded - matchStartDelta := s.decodedStart - matchIndex[0] - adjustedMatchIndex[0] = s.relativeStart - matchStartDelta - } else { - // It starts within the encoded segment so set the bound to the - // relative start - adjustedMatchIndex[0] = s.relativeStart - } - - if matchIndex[1] > s.decodedEnd { - // It ends after the encoded segment so adjust the end to match - // the location before it was decoded - matchEndDelta := matchIndex[1] - s.decodedEnd - adjustedMatchIndex[1] = s.relativeEnd + matchEndDelta - } else { - // It ends within the encoded segment so set the bound to the relative end - adjustedMatchIndex[1] = s.relativeEnd - } - - // We're still not at a root segment so we'll need to keep on adjusting - if s.parent != nil { - return s.parent.adjustMatchIndex(adjustedMatchIndex) - } - - return adjustedMatchIndex -} - -// depth reports how many levels of decoding needed to be done (default is 1) -func (s EncodedSegment) depth() int { - depth := 1 - - // Climb the tree and increment the depth - for current := &s; current.parent != nil; current = current.parent { - depth++ - } - - return depth -} - -// tags returns additional meta data tags related to the types of segments -func (s EncodedSegment) tags() []string { - return []string{ - fmt.Sprintf("decoded:%s", s.encoding), - fmt.Sprintf("decode-depth:%d", s.depth()), - } -} - -// Decoder decodes various types of data in place -type Decoder struct { - decodedMap map[string]string -} - -// NewDecoder creates a default decoder struct -func NewDecoder() *Decoder { - return &Decoder{ - decodedMap: make(map[string]string), - } -} - -// decode returns the data with the values decoded in-place -func (d *Decoder) decode(data string, parentSegments []EncodedSegment) (string, []EncodedSegment) { - segments := d.findEncodedSegments(data, parentSegments) - - if len(segments) > 0 { - result := bytes.NewBuffer(make([]byte, 0, len(data))) - - relativeStart := 0 - for _, segment := range segments { - result.WriteString(data[relativeStart:segment.relativeStart]) - result.WriteString(segment.decodedValue) - relativeStart = segment.relativeEnd - } - result.WriteString(data[relativeStart:]) - - return result.String(), segments - } - - return data, segments -} - -// findEncodedSegments finds the encoded segments in the data and updates the -// segment tree for this pass -func (d *Decoder) findEncodedSegments(data string, parentSegments []EncodedSegment) []EncodedSegment { - if len(data) == 0 { - return []EncodedSegment{} - } - - matchIndices := b64Regexp.FindAllStringIndex(data, -1) - if matchIndices == nil { - return []EncodedSegment{} - } - - segments := make([]EncodedSegment, 0, len(matchIndices)) - - // Keeps up with offsets from the text changing size as things are decoded - decodedShift := 0 - - for _, matchIndex := range matchIndices { - encodedValue := data[matchIndex[0]:matchIndex[1]] - - if !isLikelyB64(encodedValue) { - d.decodedMap[encodedValue] = "" - continue - } - - decodedValue, alreadyDecoded := d.decodedMap[encodedValue] - - // We haven't decoded this yet, so go ahead and decode it - if !alreadyDecoded { - decodedValue = decodeValue(encodedValue) - d.decodedMap[encodedValue] = decodedValue - } - - // Skip this segment because there was nothing to check - if len(decodedValue) == 0 { - continue - } - - // Create a segment for the encoded data - segment := EncodedSegment{ - relativeStart: matchIndex[0], - relativeEnd: matchIndex[1], - absoluteStart: matchIndex[0], - absoluteEnd: matchIndex[1], - decodedStart: matchIndex[0] + decodedShift, - decodedEnd: matchIndex[0] + decodedShift + len(decodedValue), - decodedValue: decodedValue, - encoding: "base64", - } - - // Shift decoded start and ends based on size changes - decodedShift += len(decodedValue) - len(encodedValue) - - // Adjust the absolute position of segments contained in parent segments - for _, parentSegment := range parentSegments { - if segment.isChildOf(parentSegment) { - segment.absoluteStart = parentSegment.absoluteStart - segment.absoluteEnd = parentSegment.absoluteEnd - segment.parent = &parentSegment - break - } - } - - logging.Debug().Msgf("segment found: %#v", segment) - segments = append(segments, segment) - } - - return segments -} - -// decoders tries a list of decoders and returns the first successful one -func decodeValue(encodedValue string) string { - for _, decoder := range decoders { - decodedValue, err := decoder(encodedValue) - - if err == nil && len(decodedValue) > 0 && isASCII(decodedValue) { - return string(decodedValue) - } - } - - return "" -} - -func isASCII(b []byte) bool { - for i := 0; i < len(b); i++ { - if b[i] > unicode.MaxASCII || b[i] < '\t' { - return false - } - } - - return true -} - -// Skip a lot of method signatures and things at the risk of missing about -// 1% of base64 -func isLikelyB64(s string) bool { - for _, c := range s { - if b64LikelyChars[c] != 0 { - return true - } - } - - return false -} - -// Find a segment where the decoded bounds overlaps a range -func segmentWithDecodedOverlap(encodedSegments []EncodedSegment, start, end int) *EncodedSegment { - for _, segment := range encodedSegments { - if segment.decodedOverlaps(start, end) { - return &segment - } - } - - return nil -} - -func (s EncodedSegment) currentLine(currentRaw string) string { - start := 0 - end := len(currentRaw) - - // Find the start of the range - for i := s.decodedStart; i > -1; i-- { - c := currentRaw[i] - if c == '\n' { - start = i - break - } - } - - // Find the end of the range - for i := s.decodedEnd; i < end; i++ { - c := currentRaw[i] - if c == '\n' { - end = i - break - } - } - - return currentRaw[start:end] -} diff --git a/cli/detect/detect.go b/cli/detect/detect.go deleted file mode 100644 index f2e42cccc..000000000 --- a/cli/detect/detect.go +++ /dev/null @@ -1,699 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package detect - -import ( - "bufio" - "context" - "fmt" - "os" - "runtime" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/Infisical/infisical-merge/detect/config" - "github.com/Infisical/infisical-merge/detect/logging" - "github.com/Infisical/infisical-merge/detect/regexp" - "github.com/Infisical/infisical-merge/detect/report" - - ahocorasick "github.com/BobuSumisu/aho-corasick" - "github.com/fatih/semgroup" - "github.com/rs/zerolog" - "github.com/spf13/viper" - "golang.org/x/exp/maps" -) - -const ( - gitleaksAllowSignature = "gitleaks:allow" - chunkSize = 100 * 1_000 // 100kb - - // SlowWarningThreshold is the amount of time to wait before logging that a file is slow. - // This is useful for identifying problematic files and tuning the allowlist. - SlowWarningThreshold = 5 * time.Second -) - -var ( - newLineRegexp = regexp.MustCompile("\n") - isWindows = runtime.GOOS == "windows" -) - -// Detector is the main detector struct -type Detector struct { - // Config is the configuration for the detector - Config config.Config - - // Redact is a flag to redact findings. This is exported - // so users using gitleaks as a library can set this flag - // without calling `detector.Start(cmd *cobra.Command)` - Redact uint - - // verbose is a flag to print findings - Verbose bool - - // MaxDecodeDepths limits how many recursive decoding passes are allowed - MaxDecodeDepth int - - // files larger than this will be skipped - MaxTargetMegaBytes int - - // followSymlinks is a flag to enable scanning symlink files - FollowSymlinks bool - - // NoColor is a flag to disable color output - NoColor bool - - // IgnoreGitleaksAllow is a flag to ignore gitleaks:allow comments. - IgnoreGitleaksAllow bool - - // commitMap is used to keep track of commits that have been scanned. - // This is only used for logging purposes and git scans. - commitMap map[string]bool - - // findingMutex is to prevent concurrent access to the - // findings slice when adding findings. - findingMutex *sync.Mutex - - // findings is a slice of report.Findings. This is the result - // of the detector's scan which can then be used to generate a - // report. - findings []report.Finding - - // prefilter is a ahocorasick struct used for doing efficient string - // matching given a set of words (keywords from the rules in the config) - prefilter ahocorasick.Trie - - // a list of known findings that should be ignored - baseline []report.Finding - - // path to baseline - baselinePath string - - // gitleaksIgnore - gitleaksIgnore map[string]struct{} - - // Sema (https://github.com/fatih/semgroup) controls the concurrency - Sema *semgroup.Group - - // report-related settings. - ReportPath string - Reporter report.Reporter - - TotalBytes atomic.Uint64 -} - -// Fragment contains the data to be scanned -type Fragment struct { - // Raw is the raw content of the fragment - Raw string - - Bytes []byte - - // FilePath is the path to the file, if applicable. - // The path separator MUST be normalized to `/`. - FilePath string - SymlinkFile string - // WindowsFilePath is the path with the original separator. - // This provides a backwards-compatible solution to https://github.com/gitleaks/gitleaks/issues/1565. - WindowsFilePath string `json:"-"` // TODO: remove this in v9. - - // CommitSHA is the SHA of the commit if applicable - CommitSHA string - - // newlineIndices is a list of indices of newlines in the raw content. - // This is used to calculate the line location of a finding - newlineIndices [][]int -} - -// NewDetector creates a new detector with the given config -func NewDetector(cfg config.Config) *Detector { - return &Detector{ - commitMap: make(map[string]bool), - gitleaksIgnore: make(map[string]struct{}), - findingMutex: &sync.Mutex{}, - findings: make([]report.Finding, 0), - Config: cfg, - prefilter: *ahocorasick.NewTrieBuilder().AddStrings(maps.Keys(cfg.Keywords)).Build(), - Sema: semgroup.NewGroup(context.Background(), 40), - } -} - -// NewDetectorDefaultConfig creates a new detector with the default config -func NewDetectorDefaultConfig() (*Detector, error) { - viper.SetConfigType("toml") - err := viper.ReadConfig(strings.NewReader(config.DefaultConfig)) - if err != nil { - return nil, err - } - var vc config.ViperConfig - err = viper.Unmarshal(&vc) - if err != nil { - return nil, err - } - cfg, err := vc.Translate() - if err != nil { - return nil, err - } - return NewDetector(cfg), nil -} - -func (d *Detector) AddGitleaksIgnore(gitleaksIgnorePath string) error { - logging.Debug().Msgf("found .gitleaksignore file: %s", gitleaksIgnorePath) - file, err := os.Open(gitleaksIgnorePath) - if err != nil { - return err - } - defer func() { - // https://github.com/securego/gosec/issues/512 - if err := file.Close(); err != nil { - logging.Warn().Msgf("Error closing .gitleaksignore file: %s\n", err) - } - }() - - scanner := bufio.NewScanner(file) - replacer := strings.NewReplacer("\\", "/") - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - // Skip lines that start with a comment - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - // Normalize the path. - // TODO: Make this a breaking change in v9. - s := strings.Split(line, ":") - switch len(s) { - case 3: - // Global fingerprint. - // `file:rule-id:start-line` - s[0] = replacer.Replace(s[0]) - case 4: - // Commit fingerprint. - // `commit:file:rule-id:start-line` - s[1] = replacer.Replace(s[1]) - default: - logging.Warn().Str("fingerprint", line).Msg("Invalid .gitleaksignore entry") - } - d.gitleaksIgnore[strings.Join(s, ":")] = struct{}{} - } - return nil -} - -// DetectBytes scans the given bytes and returns a list of findings -func (d *Detector) DetectBytes(content []byte) []report.Finding { - return d.DetectString(string(content)) -} - -// DetectString scans the given string and returns a list of findings -func (d *Detector) DetectString(content string) []report.Finding { - return d.Detect(Fragment{ - Raw: content, - }) -} - -// Detect scans the given fragment and returns a list of findings -func (d *Detector) Detect(fragment Fragment) []report.Finding { - if fragment.Bytes == nil { - d.TotalBytes.Add(uint64(len(fragment.Raw))) - } - d.TotalBytes.Add(uint64(len(fragment.Bytes))) - - var ( - findings []report.Finding - logger = func() zerolog.Logger { - l := logging.With().Str("path", fragment.FilePath) - if fragment.CommitSHA != "" { - l = l.Str("commit", fragment.CommitSHA) - } - return l.Logger() - }() - ) - - // check if filepath is allowed - if fragment.FilePath != "" { - // is the path our config or baseline file? - if fragment.FilePath == d.Config.Path || (d.baselinePath != "" && fragment.FilePath == d.baselinePath) { - logging.Trace().Msg("skipping file: matches config or baseline path") - return findings - } - } - // check if commit or filepath is allowed. - if isAllowed, event := checkCommitOrPathAllowed(logger, fragment, d.Config.Allowlists); isAllowed { - event.Msg("skipping file: global allowlist") - return findings - } - - // add newline indices for location calculation in detectRule - fragment.newlineIndices = newLineRegexp.FindAllStringIndex(fragment.Raw, -1) - - // setup variables to handle different decoding passes - currentRaw := fragment.Raw - encodedSegments := []EncodedSegment{} - currentDecodeDepth := 0 - decoder := NewDecoder() - - for { - // build keyword map for prefiltering rules - keywords := make(map[string]bool) - normalizedRaw := strings.ToLower(currentRaw) - matches := d.prefilter.MatchString(normalizedRaw) - for _, m := range matches { - keywords[normalizedRaw[m.Pos():int(m.Pos())+len(m.Match())]] = true - } - - for _, rule := range d.Config.Rules { - if len(rule.Keywords) == 0 { - // if no keywords are associated with the rule always scan the - // fragment using the rule - findings = append(findings, d.detectRule(fragment, currentRaw, rule, encodedSegments)...) - continue - } - - // check if keywords are in the fragment - for _, k := range rule.Keywords { - if _, ok := keywords[strings.ToLower(k)]; ok { - findings = append(findings, d.detectRule(fragment, currentRaw, rule, encodedSegments)...) - break - } - } - } - - // increment the depth by 1 as we start our decoding pass - currentDecodeDepth++ - - // stop the loop if we've hit our max decoding depth - if currentDecodeDepth > d.MaxDecodeDepth { - break - } - - // decode the currentRaw for the next pass - currentRaw, encodedSegments = decoder.decode(currentRaw, encodedSegments) - - // stop the loop when there's nothing else to decode - if len(encodedSegments) == 0 { - break - } - } - - return filter(findings, d.Redact) -} - -// detectRule scans the given fragment for the given rule and returns a list of findings -func (d *Detector) detectRule(fragment Fragment, currentRaw string, r config.Rule, encodedSegments []EncodedSegment) []report.Finding { - var ( - findings []report.Finding - logger = func() zerolog.Logger { - l := logging.With().Str("rule-id", r.RuleID).Str("path", fragment.FilePath) - if fragment.CommitSHA != "" { - l = l.Str("commit", fragment.CommitSHA) - } - return l.Logger() - }() - ) - - // check if commit or file is allowed for this rule. - if isAllowed, event := checkCommitOrPathAllowed(logger, fragment, r.Allowlists); isAllowed { - event.Msg("skipping file: rule allowlist") - return findings - } - - if r.Path != nil { - if r.Regex == nil && len(encodedSegments) == 0 { - // Path _only_ rule - if r.Path.MatchString(fragment.FilePath) || (fragment.WindowsFilePath != "" && r.Path.MatchString(fragment.WindowsFilePath)) { - finding := report.Finding{ - RuleID: r.RuleID, - Description: r.Description, - File: fragment.FilePath, - SymlinkFile: fragment.SymlinkFile, - Match: fmt.Sprintf("file detected: %s", fragment.FilePath), - Tags: r.Tags, - } - return append(findings, finding) - } - } else { - // if path is set _and_ a regex is set, then we need to check both - // so if the path does not match, then we should return early and not - // consider the regex - if !(r.Path.MatchString(fragment.FilePath) || (fragment.WindowsFilePath != "" && r.Path.MatchString(fragment.WindowsFilePath))) { - return findings - } - } - } - - // if path only rule, skip content checks - if r.Regex == nil { - return findings - } - - // if flag configure and raw data size bigger then the flag - if d.MaxTargetMegaBytes > 0 { - rawLength := len(currentRaw) / 1000000 - if rawLength > d.MaxTargetMegaBytes { - logger.Debug(). - Int("size", rawLength). - Int("max-size", d.MaxTargetMegaBytes). - Msg("skipping fragment: size") - return findings - } - } - - // use currentRaw instead of fragment.Raw since this represents the current - // decoding pass on the text - for _, matchIndex := range r.Regex.FindAllStringIndex(currentRaw, -1) { - // Extract secret from match - secret := strings.Trim(currentRaw[matchIndex[0]:matchIndex[1]], "\n") - - // For any meta data from decoding - var metaTags []string - currentLine := "" - - // Check if the decoded portions of the segment overlap with the match - // to see if its potentially a new match - if len(encodedSegments) > 0 { - if segment := segmentWithDecodedOverlap(encodedSegments, matchIndex[0], matchIndex[1]); segment != nil { - matchIndex = segment.adjustMatchIndex(matchIndex) - metaTags = append(metaTags, segment.tags()...) - currentLine = segment.currentLine(currentRaw) - } else { - // This item has already been added to a finding - continue - } - } else { - // Fixes: https://github.com/gitleaks/gitleaks/issues/1352 - // removes the incorrectly following line that was detected by regex expression '\n' - matchIndex[1] = matchIndex[0] + len(secret) - } - - // determine location of match. Note that the location - // in the finding will be the line/column numbers of the _match_ - // not the _secret_, which will be different if the secretGroup - // value is set for this rule - loc := location(fragment, matchIndex) - - if matchIndex[1] > loc.endLineIndex { - loc.endLineIndex = matchIndex[1] - } - - finding := report.Finding{ - RuleID: r.RuleID, - Description: r.Description, - StartLine: loc.startLine, - EndLine: loc.endLine, - StartColumn: loc.startColumn, - EndColumn: loc.endColumn, - Line: fragment.Raw[loc.startLineIndex:loc.endLineIndex], - Match: secret, - Secret: secret, - File: fragment.FilePath, - SymlinkFile: fragment.SymlinkFile, - Tags: append(r.Tags, metaTags...), - } - - if !d.IgnoreGitleaksAllow && strings.Contains(finding.Line, gitleaksAllowSignature) { - logger.Trace(). - Str("finding", finding.Secret). - Msg("skipping finding: 'gitleaks:allow' signature") - continue - } - - if currentLine == "" { - currentLine = finding.Line - } - - // Set the value of |secret|, if the pattern contains at least one capture group. - // (The first element is the full match, hence we check >= 2.) - groups := r.Regex.FindStringSubmatch(finding.Secret) - if len(groups) >= 2 { - if r.SecretGroup > 0 { - if len(groups) <= r.SecretGroup { - // Config validation should prevent this - continue - } - finding.Secret = groups[r.SecretGroup] - } else { - // If |secretGroup| is not set, we will use the first suitable capture group. - for _, s := range groups[1:] { - if len(s) > 0 { - finding.Secret = s - break - } - } - } - } - - // check entropy - entropy := shannonEntropy(finding.Secret) - finding.Entropy = float32(entropy) - if r.Entropy != 0.0 { - // entropy is too low, skip this finding - if entropy <= r.Entropy { - logger.Trace(). - Str("finding", finding.Secret). - Float32("entropy", finding.Entropy). - Msg("skipping finding: low entropy") - continue - } - } - - // check if the result matches any of the global allowlists. - if isAllowed, event := checkFindingAllowed(logger, finding, fragment, currentLine, d.Config.Allowlists); isAllowed { - event.Msg("skipping finding: global allowlist") - continue - } - - // check if the result matches any of the rule allowlists. - if isAllowed, event := checkFindingAllowed(logger, finding, fragment, currentLine, r.Allowlists); isAllowed { - event.Msg("skipping finding: rule allowlist") - continue - } - findings = append(findings, finding) - } - return findings -} - -// AddFinding synchronously adds a finding to the findings slice -func (d *Detector) AddFinding(finding report.Finding) { - globalFingerprint := fmt.Sprintf("%s:%s:%d", finding.File, finding.RuleID, finding.StartLine) - if finding.Commit != "" { - finding.Fingerprint = fmt.Sprintf("%s:%s:%s:%d", finding.Commit, finding.File, finding.RuleID, finding.StartLine) - } else { - finding.Fingerprint = globalFingerprint - } - - // check if we should ignore this finding - logger := logging.With().Str("finding", finding.Secret).Logger() - if _, ok := d.gitleaksIgnore[globalFingerprint]; ok { - logger.Debug(). - Str("fingerprint", globalFingerprint). - Msg("skipping finding: global fingerprint") - return - } else if finding.Commit != "" { - // Awkward nested if because I'm not sure how to chain these two conditions. - if _, ok := d.gitleaksIgnore[finding.Fingerprint]; ok { - logger.Debug(). - Str("fingerprint", finding.Fingerprint). - Msgf("skipping finding: fingerprint") - return - } - } - - if d.baseline != nil && !IsNew(finding, d.Redact, d.baseline) { - logger.Debug(). - Str("fingerprint", finding.Fingerprint). - Msgf("skipping finding: baseline") - return - } - - d.findingMutex.Lock() - d.findings = append(d.findings, finding) - if d.Verbose { - printFinding(finding, d.NoColor) - } - d.findingMutex.Unlock() -} - -// Findings returns the findings added to the detector -func (d *Detector) Findings() []report.Finding { - return d.findings -} - -// AddCommit synchronously adds a commit to the commit slice -func (d *Detector) addCommit(commit string) { - d.commitMap[commit] = true -} - -// checkCommitOrPathAllowed evaluates |fragment| against all provided |allowlists|. -// -// If the match condition is "OR", only commit and path are checked. -// Otherwise, if regexes or stopwords are defined this will fail. -func checkCommitOrPathAllowed( - logger zerolog.Logger, - fragment Fragment, - allowlists []*config.Allowlist, -) (bool, *zerolog.Event) { - if fragment.FilePath == "" && fragment.CommitSHA == "" { - return false, nil - } - - for _, a := range allowlists { - var ( - isAllowed bool - allowlistChecks []bool - commitAllowed, _ = a.CommitAllowed(fragment.CommitSHA) - pathAllowed = a.PathAllowed(fragment.FilePath) || (fragment.WindowsFilePath != "" && a.PathAllowed(fragment.WindowsFilePath)) - ) - // If the condition is "AND" we need to check all conditions. - if a.MatchCondition == config.AllowlistMatchAnd { - if len(a.Commits) > 0 { - allowlistChecks = append(allowlistChecks, commitAllowed) - } - if len(a.Paths) > 0 { - allowlistChecks = append(allowlistChecks, pathAllowed) - } - // These will be checked later. - if len(a.Regexes) > 0 { - continue - } - if len(a.StopWords) > 0 { - continue - } - - isAllowed = allTrue(allowlistChecks) - } else { - isAllowed = commitAllowed || pathAllowed - } - if isAllowed { - event := logger.Trace().Str("condition", a.MatchCondition.String()) - if commitAllowed { - event.Bool("allowed-commit", commitAllowed) - } - if pathAllowed { - event.Bool("allowed-path", pathAllowed) - } - return true, event - } - } - return false, nil -} - -// checkFindingAllowed evaluates |finding| against all provided |allowlists|. -// -// If the match condition is "OR", only regex and stopwords are run. (Commit and path should be handled separately). -// Otherwise, all conditions are checked. -// -// TODO: The method signature is awkward. I can't think of a better way to log helpful info. -func checkFindingAllowed( - logger zerolog.Logger, - finding report.Finding, - fragment Fragment, - currentLine string, - allowlists []*config.Allowlist, -) (bool, *zerolog.Event) { - for _, a := range allowlists { - allowlistTarget := finding.Secret - switch a.RegexTarget { - case "match": - allowlistTarget = finding.Match - case "line": - allowlistTarget = currentLine - } - - var ( - checks []bool - isAllowed bool - commitAllowed bool - commit string - pathAllowed bool - regexAllowed = a.RegexAllowed(allowlistTarget) - containsStopword, word = a.ContainsStopWord(finding.Secret) - ) - // If the condition is "AND" we need to check all conditions. - if a.MatchCondition == config.AllowlistMatchAnd { - // Determine applicable checks. - if len(a.Commits) > 0 { - commitAllowed, commit = a.CommitAllowed(fragment.CommitSHA) - checks = append(checks, commitAllowed) - } - if len(a.Paths) > 0 { - pathAllowed = a.PathAllowed(fragment.FilePath) || (fragment.WindowsFilePath != "" && a.PathAllowed(fragment.WindowsFilePath)) - checks = append(checks, pathAllowed) - } - if len(a.Regexes) > 0 { - checks = append(checks, regexAllowed) - } - if len(a.StopWords) > 0 { - checks = append(checks, containsStopword) - } - - isAllowed = allTrue(checks) - } else { - isAllowed = regexAllowed || containsStopword - } - - if isAllowed { - event := logger.Trace(). - Str("finding", finding.Secret). - Str("condition", a.MatchCondition.String()) - if commitAllowed { - event.Str("allowed-commit", commit) - } - if pathAllowed { - event.Bool("allowed-path", pathAllowed) - } - if regexAllowed { - event.Bool("allowed-regex", regexAllowed) - } - if containsStopword { - event.Str("allowed-stopword", word) - } - return true, event - } - } - return false, nil -} - -func allTrue(bools []bool) bool { - for _, check := range bools { - if !check { - return false - } - } - return true -} - -func fileExists(fileName string) bool { - // check for a .infisicalignore file - info, err := os.Stat(fileName) - if err != nil && !os.IsNotExist(err) { - return false - } - - if info != nil && err == nil { - if !info.IsDir() { - return true - } - } - return false -} diff --git a/cli/detect/directory.go b/cli/detect/directory.go deleted file mode 100644 index 56f4999f2..000000000 --- a/cli/detect/directory.go +++ /dev/null @@ -1,225 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package detect - -import ( - "bufio" - "bytes" - "io" - "os" - "path/filepath" - "strings" - "time" - - "github.com/h2non/filetype" - - "github.com/Infisical/infisical-merge/detect/logging" - "github.com/Infisical/infisical-merge/detect/report" - "github.com/Infisical/infisical-merge/detect/sources" -) - -const maxPeekSize = 25 * 1_000 // 10kb - -func (d *Detector) DetectFiles(paths <-chan sources.ScanTarget) ([]report.Finding, error) { - for pa := range paths { - d.Sema.Go(func() error { - logger := logging.With().Str("path", pa.Path).Logger() - logger.Trace().Msg("Scanning path") - - f, err := os.Open(pa.Path) - if err != nil { - if os.IsPermission(err) { - logger.Warn().Msg("Skipping file: permission denied") - return nil - } - return err - } - defer func() { - _ = f.Close() - }() - - // Get file size - fileInfo, err := f.Stat() - if err != nil { - return err - } - fileSize := fileInfo.Size() - if d.MaxTargetMegaBytes > 0 { - rawLength := fileSize / 1000000 - if rawLength > int64(d.MaxTargetMegaBytes) { - logger.Debug(). - Int64("size", rawLength). - Msg("Skipping file: exceeds --max-target-megabytes") - return nil - } - } - - var ( - // Buffer to hold file chunks - reader = bufio.NewReaderSize(f, chunkSize) - buf = make([]byte, chunkSize) - totalLines = 0 - ) - for { - n, err := reader.Read(buf) - - // "Callers should always process the n > 0 bytes returned before considering the error err." - // https://pkg.go.dev/io#Reader - if n > 0 { - // Only check the filetype at the start of file. - if totalLines == 0 { - // TODO: could other optimizations be introduced here? - if mimetype, err := filetype.Match(buf[:n]); err != nil { - return nil - } else if mimetype.MIME.Type == "application" { - return nil // skip binary files - } - } - - // Try to split chunks across large areas of whitespace, if possible. - peekBuf := bytes.NewBuffer(buf[:n]) - if readErr := readUntilSafeBoundary(reader, n, maxPeekSize, peekBuf); readErr != nil { - return readErr - } - - // Count the number of newlines in this chunk - chunk := peekBuf.String() - linesInChunk := strings.Count(chunk, "\n") - totalLines += linesInChunk - fragment := Fragment{ - Raw: chunk, - Bytes: peekBuf.Bytes(), - } - if pa.Symlink != "" { - fragment.SymlinkFile = pa.Symlink - } - - if isWindows { - fragment.FilePath = filepath.ToSlash(pa.Path) - fragment.SymlinkFile = filepath.ToSlash(fragment.SymlinkFile) - fragment.WindowsFilePath = pa.Path - } else { - fragment.FilePath = pa.Path - } - - timer := time.AfterFunc(SlowWarningThreshold, func() { - logger.Debug().Msgf("Taking longer than %s to inspect fragment", SlowWarningThreshold.String()) - }) - for _, finding := range d.Detect(fragment) { - // need to add 1 since line counting starts at 1 - finding.StartLine += (totalLines - linesInChunk) + 1 - finding.EndLine += (totalLines - linesInChunk) + 1 - d.AddFinding(finding) - } - if timer != nil { - timer.Stop() - timer = nil - } - } - - if err != nil { - if err == io.EOF { - return nil - } - return err - } - } - }) - } - - if err := d.Sema.Wait(); err != nil { - return d.findings, err - } - - return d.findings, nil -} - -// readUntilSafeBoundary consumes |f| until it finds two consecutive `\n` characters, up to |maxPeekSize|. -// This hopefully avoids splitting. (https://github.com/gitleaks/gitleaks/issues/1651) -func readUntilSafeBoundary(r *bufio.Reader, n int, maxPeekSize int, peekBuf *bytes.Buffer) error { - if peekBuf.Len() == 0 { - return nil - } - - // Does the buffer end in consecutive newlines? - var ( - data = peekBuf.Bytes() - lastChar = data[len(data)-1] - newlineCount = 0 // Tracks consecutive newlines - ) - if isWhitespace(lastChar) { - for i := len(data) - 1; i >= 0; i-- { - lastChar = data[i] - if lastChar == '\n' { - newlineCount++ - - // Stop if two consecutive newlines are found - if newlineCount >= 2 { - return nil - } - } else if lastChar == '\r' || lastChar == ' ' || lastChar == '\t' { - // The presence of other whitespace characters (`\r`, ` `, `\t`) shouldn't reset the count. - // (Intentionally do nothing.) - } else { - break - } - } - } - - // If not, read ahead until we (hopefully) find some. - newlineCount = 0 - for { - data = peekBuf.Bytes() - // Check if the last character is a newline. - lastChar = data[len(data)-1] - if lastChar == '\n' { - newlineCount++ - - // Stop if two consecutive newlines are found - if newlineCount >= 2 { - break - } - } else if lastChar == '\r' || lastChar == ' ' || lastChar == '\t' { - // The presence of other whitespace characters (`\r`, ` `, `\t`) shouldn't reset the count. - // (Intentionally do nothing.) - } else { - newlineCount = 0 // Reset if a non-newline character is found - } - - // Stop growing the buffer if it reaches maxSize - if (peekBuf.Len() - n) >= maxPeekSize { - break - } - - // Read additional data into a temporary buffer - b, err := r.ReadByte() - if err != nil { - if err == io.EOF { - break - } - return err - } - peekBuf.WriteByte(b) - } - return nil -} diff --git a/cli/detect/git.go b/cli/detect/git.go deleted file mode 100644 index 83ed8a853..000000000 --- a/cli/detect/git.go +++ /dev/null @@ -1,216 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package detect - -import ( - "bytes" - "errors" - "fmt" - "net/url" - "os/exec" - "regexp" - "strings" - "time" - - "github.com/Infisical/infisical-merge/detect/cmd/scm" - "github.com/gitleaks/go-gitdiff/gitdiff" - - "github.com/Infisical/infisical-merge/detect/logging" - "github.com/Infisical/infisical-merge/detect/report" - "github.com/Infisical/infisical-merge/detect/sources" -) - -func (d *Detector) DetectGit(cmd *sources.GitCmd, remote *RemoteInfo) ([]report.Finding, error) { - defer cmd.Wait() - var ( - diffFilesCh = cmd.DiffFilesCh() - errCh = cmd.ErrCh() - ) - - // loop to range over both DiffFiles (stdout) and ErrCh (stderr) - for diffFilesCh != nil || errCh != nil { - select { - case gitdiffFile, open := <-diffFilesCh: - if !open { - diffFilesCh = nil - break - } - - // skip binary files - if gitdiffFile.IsBinary || gitdiffFile.IsDelete { - continue - } - - // Check if commit is allowed - commitSHA := "" - if gitdiffFile.PatchHeader != nil { - commitSHA = gitdiffFile.PatchHeader.SHA - for _, a := range d.Config.Allowlists { - if ok, c := a.CommitAllowed(gitdiffFile.PatchHeader.SHA); ok { - logging.Trace().Str("allowed-commit", c).Msg("skipping commit: global allowlist") - continue - } - } - } - d.addCommit(commitSHA) - - d.Sema.Go(func() error { - for _, textFragment := range gitdiffFile.TextFragments { - if textFragment == nil { - return nil - } - - fragment := Fragment{ - Raw: textFragment.Raw(gitdiff.OpAdd), - CommitSHA: commitSHA, - FilePath: gitdiffFile.NewName, - } - - timer := time.AfterFunc(SlowWarningThreshold, func() { - logging.Debug(). - Str("commit", commitSHA[:7]). - Str("path", fragment.FilePath). - Msgf("Taking longer than %s to inspect fragment", SlowWarningThreshold.String()) - }) - for _, finding := range d.Detect(fragment) { - d.AddFinding(augmentGitFinding(remote, finding, textFragment, gitdiffFile)) - } - if timer != nil { - timer.Stop() - timer = nil - } - } - return nil - }) - case err, open := <-errCh: - if !open { - errCh = nil - break - } - - return d.findings, err - } - } - - if err := d.Sema.Wait(); err != nil { - return d.findings, err - } - logging.Info().Msgf("%d commits scanned.", len(d.commitMap)) - logging.Debug().Msg("Note: this number might be smaller than expected due to commits with no additions") - return d.findings, nil -} - -type RemoteInfo struct { - Platform scm.Platform - Url string -} - -func NewRemoteInfo(platform scm.Platform, source string) *RemoteInfo { - if platform == scm.NoPlatform { - return &RemoteInfo{Platform: platform} - } - - remoteUrl, err := getRemoteUrl(source) - if err != nil { - if strings.Contains(err.Error(), "No remote configured") { - logging.Debug().Msg("skipping finding links: repository has no configured remote.") - platform = scm.NoPlatform - } else { - logging.Error().Err(err).Msg("skipping finding links: unable to parse remote URL") - } - goto End - } - - if platform == scm.UnknownPlatform { - platform = platformFromHost(remoteUrl) - if platform == scm.UnknownPlatform { - logging.Info(). - Str("host", remoteUrl.Hostname()). - Msg("Unknown SCM platform. Use --platform to include links in findings.") - } else { - logging.Debug(). - Str("host", remoteUrl.Hostname()). - Str("platform", platform.String()). - Msg("SCM platform parsed from host") - } - } - -End: - var rUrl string - if remoteUrl != nil { - rUrl = remoteUrl.String() - } - return &RemoteInfo{ - Platform: platform, - Url: rUrl, - } -} - -var sshUrlpat = regexp.MustCompile(`^git@([a-zA-Z0-9.-]+):([\w/.-]+?)(?:\.git)?$`) - -func getRemoteUrl(source string) (*url.URL, error) { - // This will return the first remote — typically, "origin". - cmd := exec.Command("git", "ls-remote", "--quiet", "--get-url") - if source != "." { - cmd.Dir = source - } - - stdout, err := cmd.Output() - if err != nil { - var exitError *exec.ExitError - if errors.As(err, &exitError) { - return nil, fmt.Errorf("command failed (%d): %w, stderr: %s", exitError.ExitCode(), err, string(bytes.TrimSpace(exitError.Stderr))) - } - return nil, err - } - - remoteUrl := string(bytes.TrimSpace(stdout)) - if matches := sshUrlpat.FindStringSubmatch(remoteUrl); matches != nil { - remoteUrl = fmt.Sprintf("https://%s/%s", matches[1], matches[2]) - } - remoteUrl = strings.TrimSuffix(remoteUrl, ".git") - - parsedUrl, err := url.Parse(remoteUrl) - if err != nil { - return nil, fmt.Errorf("unable to parse remote URL: %w", err) - } - - // Remove any user info. - parsedUrl.User = nil - return parsedUrl, nil -} - -func platformFromHost(u *url.URL) scm.Platform { - switch strings.ToLower(u.Hostname()) { - case "github.com": - return scm.GitHubPlatform - case "gitlab.com": - return scm.GitLabPlatform - case "dev.azure.com", "visualstudio.com": - return scm.AzureDevOpsPlatform - case "bitbucket.org": - return scm.BitBucketPlatform - default: - return scm.UnknownPlatform - } -} diff --git a/cli/detect/location.go b/cli/detect/location.go deleted file mode 100644 index 81419511c..000000000 --- a/cli/detect/location.go +++ /dev/null @@ -1,102 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package detect - -// Location represents a location in a file -type Location struct { - startLine int - endLine int - startColumn int - endColumn int - startLineIndex int - endLineIndex int -} - -func location(fragment Fragment, matchIndex []int) Location { - var ( - prevNewLine int - location Location - lineSet bool - _lineNum int - ) - - start := matchIndex[0] - end := matchIndex[1] - - // default startLineIndex to 0 - location.startLineIndex = 0 - - // Fixes: https://github.com/zricethezav/gitleaks/issues/1037 - // When a fragment does NOT have any newlines, a default "newline" - // will be counted to make the subsequent location calculation logic work - // for fragments will no newlines. - if len(fragment.newlineIndices) == 0 { - fragment.newlineIndices = [][]int{ - {len(fragment.Raw), len(fragment.Raw) + 1}, - } - } - - for lineNum, pair := range fragment.newlineIndices { - _lineNum = lineNum - newLineByteIndex := pair[0] - if prevNewLine <= start && start < newLineByteIndex { - lineSet = true - location.startLine = lineNum - location.endLine = lineNum - location.startColumn = (start - prevNewLine) + 1 // +1 because counting starts at 1 - location.startLineIndex = prevNewLine - location.endLineIndex = newLineByteIndex - } - if prevNewLine < end && end <= newLineByteIndex { - location.endLine = lineNum - location.endColumn = (end - prevNewLine) - location.endLineIndex = newLineByteIndex - } - - prevNewLine = pair[0] - } - - if !lineSet { - // if lines never get set then that means the secret is most likely - // on the last line of the diff output and the diff output does not have - // a newline - location.startColumn = (start - prevNewLine) + 1 // +1 because counting starts at 1 - location.endColumn = (end - prevNewLine) - location.startLine = _lineNum + 1 - location.endLine = _lineNum + 1 - - // search for new line byte index - i := 0 - for end+i < len(fragment.Raw) { - if fragment.Raw[end+i] == '\n' { - break - } - if fragment.Raw[end+i] == '\r' { - break - } - i++ - } - location.endLineIndex = end + i - } - return location -} diff --git a/cli/detect/logging/log.go b/cli/detect/logging/log.go deleted file mode 100644 index efac01725..000000000 --- a/cli/detect/logging/log.go +++ /dev/null @@ -1,72 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package logging - -import ( - "os" - - "github.com/rs/zerolog" -) - -var Logger zerolog.Logger - -func init() { - // send all logs to stdout - Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}). - Level(zerolog.InfoLevel). - With().Timestamp().Logger() -} - -func With() zerolog.Context { - return Logger.With() -} - -func Trace() *zerolog.Event { - return Logger.Trace() -} - -func Debug() *zerolog.Event { - return Logger.Debug() -} -func Info() *zerolog.Event { - return Logger.Info() -} -func Warn() *zerolog.Event { - return Logger.Warn() -} - -func Error() *zerolog.Event { - return Logger.Error() -} - -func Err(err error) *zerolog.Event { - return Logger.Err(err) -} - -func Fatal() *zerolog.Event { - return Logger.Fatal() -} - -func Panic() *zerolog.Event { - return Logger.Panic() -} diff --git a/cli/detect/reader.go b/cli/detect/reader.go deleted file mode 100644 index d3559b68a..000000000 --- a/cli/detect/reader.go +++ /dev/null @@ -1,149 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package detect - -import ( - "bufio" - "bytes" - "errors" - "io" - - "github.com/Infisical/infisical-merge/detect/report" -) - -// DetectReader accepts an io.Reader and a buffer size for the reader in KB -func (d *Detector) DetectReader(r io.Reader, bufSize int) ([]report.Finding, error) { - reader := bufio.NewReader(r) - buf := make([]byte, 1000*bufSize) - findings := []report.Finding{} - - for { - n, err := reader.Read(buf) - - // "Callers should always process the n > 0 bytes returned before considering the error err." - // https://pkg.go.dev/io#Reader - if n > 0 { - // Try to split chunks across large areas of whitespace, if possible. - peekBuf := bytes.NewBuffer(buf[:n]) - if readErr := readUntilSafeBoundary(reader, n, maxPeekSize, peekBuf); readErr != nil { - return findings, readErr - } - - fragment := Fragment{ - Raw: peekBuf.String(), - } - for _, finding := range d.Detect(fragment) { - findings = append(findings, finding) - if d.Verbose { - printFinding(finding, d.NoColor) - } - } - } - - if err != nil { - if err == io.EOF { - break - } - return findings, err - } - } - - return findings, nil -} - -// StreamDetectReader streams the detection results from the provided io.Reader. -// It reads data using the specified buffer size (in KB) and processes each chunk through -// the existing detection logic. Findings are sent down the returned findings channel as soon as -// they are detected, while a separate error channel signals a terminal error (or nil upon successful completion). -// The function returns two channels: -// - findingsCh: a receive-only channel that emits report.Finding objects as they are found. -// - errCh: a receive-only channel that emits a single final error (or nil if no error occurred) -// once the stream ends. -// -// Recommended Usage: -// -// Since there will only ever be a single value on the errCh, it is recommended to consume the findingsCh -// first. Once findingsCh is closed, the consumer should then read from errCh to determine -// if the stream completed successfully or if an error occurred. -// -// This design avoids the need for a select loop, keeping client code simple. -// -// Example: -// -// // Assume detector is an instance of *Detector and myReader implements io.Reader. -// findingsCh, errCh := detector.StreamDetectReader(myReader, 64) // using 64 KB buffer size -// -// // Process findings as they arrive. -// for finding := range findingsCh { -// fmt.Printf("Found secret: %+v\n", finding) -// } -// -// // After the findings channel is closed, check the final error. -// if err := <-errCh; err != nil { -// log.Fatalf("StreamDetectReader encountered an error: %v", err) -// } else { -// fmt.Println("Scanning completed successfully.") -// } -func (d *Detector) StreamDetectReader(r io.Reader, bufSize int) (<-chan report.Finding, <-chan error) { - findingsCh := make(chan report.Finding, 1) - errCh := make(chan error, 1) - - go func() { - defer close(findingsCh) - defer close(errCh) - - reader := bufio.NewReader(r) - buf := make([]byte, 1000*bufSize) - - for { - n, err := reader.Read(buf) - - if n > 0 { - peekBuf := bytes.NewBuffer(buf[:n]) - if readErr := readUntilSafeBoundary(reader, n, maxPeekSize, peekBuf); readErr != nil { - errCh <- readErr - return - } - - fragment := Fragment{Raw: peekBuf.String()} - for _, finding := range d.Detect(fragment) { - findingsCh <- finding - if d.Verbose { - printFinding(finding, d.NoColor) - } - } - } - - if err != nil { - if errors.Is(err, io.EOF) { - errCh <- nil - return - } - errCh <- err - return - } - } - }() - - return findingsCh, errCh -} diff --git a/cli/detect/regexp/stdlib_regex.go b/cli/detect/regexp/stdlib_regex.go deleted file mode 100644 index 81e2089b7..000000000 --- a/cli/detect/regexp/stdlib_regex.go +++ /dev/null @@ -1,37 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -//go:build !gore2regex - -package regexp - -import ( - re "regexp" -) - -const Version = "stdlib" - -type Regexp = re.Regexp - -func MustCompile(str string) *re.Regexp { - return re.MustCompile(str) -} diff --git a/cli/detect/regexp/wasilibs_regex.go b/cli/detect/regexp/wasilibs_regex.go deleted file mode 100644 index bc64fb14b..000000000 --- a/cli/detect/regexp/wasilibs_regex.go +++ /dev/null @@ -1,37 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -//go:build gore2regex - -package regexp - -import ( - re "github.com/wasilibs/go-re2" -) - -const Version = "github.com/wasilibs/go-re2" - -type Regexp = re.Regexp - -func MustCompile(str string) *re.Regexp { - return re.MustCompile(str) -} diff --git a/cli/detect/report/constants.go b/cli/detect/report/constants.go deleted file mode 100644 index c4f06a9a3..000000000 --- a/cli/detect/report/constants.go +++ /dev/null @@ -1,26 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package report - -const version = "v8.0.0" -const driver = "gitleaks" diff --git a/cli/detect/report/csv.go b/cli/detect/report/csv.go deleted file mode 100644 index 1f8812f97..000000000 --- a/cli/detect/report/csv.go +++ /dev/null @@ -1,100 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package report - -import ( - "encoding/csv" - "io" - "strconv" - "strings" -) - -type CsvReporter struct { -} - -var _ Reporter = (*CsvReporter)(nil) - -func (r *CsvReporter) Write(w io.WriteCloser, findings []Finding) error { - if len(findings) == 0 { - return nil - } - - var ( - cw = csv.NewWriter(w) - err error - ) - columns := []string{"RuleID", - "Commit", - "File", - "SymlinkFile", - "Secret", - "Match", - "StartLine", - "EndLine", - "StartColumn", - "EndColumn", - "Author", - "Message", - "Date", - "Email", - "Fingerprint", - "Tags", - } - // A miserable attempt at "omitempty" so tests don't yell at me. - if findings[0].Link != "" { - columns = append(columns, "Link") - } - - if err = cw.Write(columns); err != nil { - return err - } - for _, f := range findings { - row := []string{f.RuleID, - f.Commit, - f.File, - f.SymlinkFile, - f.Secret, - f.Match, - strconv.Itoa(f.StartLine), - strconv.Itoa(f.EndLine), - strconv.Itoa(f.StartColumn), - strconv.Itoa(f.EndColumn), - f.Author, - f.Message, - f.Date, - f.Email, - f.Fingerprint, - strings.Join(f.Tags, " "), - } - if findings[0].Link != "" { - row = append(row, f.Link) - } - - if err = cw.Write(row); err != nil { - return err - } - } - - cw.Flush() - return cw.Error() -} diff --git a/cli/detect/report/finding.go b/cli/detect/report/finding.go deleted file mode 100644 index c53f16ee7..000000000 --- a/cli/detect/report/finding.go +++ /dev/null @@ -1,92 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package report - -import ( - "math" - "strings" -) - -// Finding contains information about strings that -// have been captured by a tree-sitter query. -type Finding struct { - // Rule is the name of the rule that was matched - RuleID string - Description string - - StartLine int - EndLine int - StartColumn int - EndColumn int - - Line string `json:"-"` - - Match string - - // Secret contains the full content of what is matched in - // the tree-sitter query. - Secret string - - // File is the name of the file containing the finding - File string - SymlinkFile string - Commit string - Link string `json:",omitempty"` - - // Entropy is the shannon entropy of Value - Entropy float32 - - Author string - Email string - Date string - Message string - Tags []string - - // unique identifier - Fingerprint string -} - -// Redact removes sensitive information from a finding. -func (f *Finding) Redact(percent uint) { - secret := maskSecret(f.Secret, percent) - if percent >= 100 { - secret = "REDACTED" - } - f.Line = strings.Replace(f.Line, f.Secret, secret, -1) - f.Match = strings.Replace(f.Match, f.Secret, secret, -1) - f.Secret = secret -} - -func maskSecret(secret string, percent uint) string { - if percent > 100 { - percent = 100 - } - len := float64(len(secret)) - if len <= 0 { - return secret - } - prc := float64(100 - percent) - lth := int64(math.RoundToEven(len * prc / float64(100))) - - return secret[:lth] + "..." -} diff --git a/cli/detect/report/json.go b/cli/detect/report/json.go deleted file mode 100644 index f47b7eee0..000000000 --- a/cli/detect/report/json.go +++ /dev/null @@ -1,39 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package report - -import ( - "encoding/json" - "io" -) - -type JsonReporter struct { -} - -var _ Reporter = (*JsonReporter)(nil) - -func (t *JsonReporter) Write(w io.WriteCloser, findings []Finding) error { - encoder := json.NewEncoder(w) - encoder.SetIndent("", " ") - return encoder.Encode(findings) -} diff --git a/cli/detect/report/junit.go b/cli/detect/report/junit.go deleted file mode 100644 index 0862a45f1..000000000 --- a/cli/detect/report/junit.go +++ /dev/null @@ -1,129 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package report - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "io" - "strconv" -) - -type JunitReporter struct { -} - -var _ Reporter = (*JunitReporter)(nil) - -func (r *JunitReporter) Write(w io.WriteCloser, findings []Finding) error { - testSuites := TestSuites{ - TestSuites: getTestSuites(findings), - } - - io.WriteString(w, xml.Header) - encoder := xml.NewEncoder(w) - encoder.Indent("", "\t") - return encoder.Encode(testSuites) -} - -func getTestSuites(findings []Finding) []TestSuite { - return []TestSuite{ - { - Failures: strconv.Itoa(len(findings)), - Name: "gitleaks", - Tests: strconv.Itoa(len(findings)), - TestCases: getTestCases(findings), - Time: "", - }, - } -} - -func getTestCases(findings []Finding) []TestCase { - testCases := []TestCase{} - for _, f := range findings { - testCase := TestCase{ - Classname: f.Description, - Failure: getFailure(f), - File: f.File, - Name: getMessage(f), - Time: "", - } - testCases = append(testCases, testCase) - } - return testCases -} - -func getFailure(f Finding) Failure { - return Failure{ - Data: getData(f), - Message: getMessage(f), - Type: f.Description, - } -} - -func getData(f Finding) string { - data, err := json.MarshalIndent(f, "", "\t") - if err != nil { - fmt.Println(err) - return "" - } - return string(data) -} - -func getMessage(f Finding) string { - if f.Commit == "" { - return fmt.Sprintf("%s has detected a secret in file %s, line %s.", f.RuleID, f.File, strconv.Itoa(f.StartLine)) - } - - return fmt.Sprintf("%s has detected a secret in file %s, line %s, at commit %s.", f.RuleID, f.File, strconv.Itoa(f.StartLine), f.Commit) -} - -type TestSuites struct { - XMLName xml.Name `xml:"testsuites"` - TestSuites []TestSuite -} - -type TestSuite struct { - XMLName xml.Name `xml:"testsuite"` - Failures string `xml:"failures,attr"` - Name string `xml:"name,attr"` - Tests string `xml:"tests,attr"` - TestCases []TestCase `xml:"testcase"` - Time string `xml:"time,attr"` -} - -type TestCase struct { - XMLName xml.Name `xml:"testcase"` - Classname string `xml:"classname,attr"` - Failure Failure `xml:"failure"` - File string `xml:"file,attr"` - Name string `xml:"name,attr"` - Time string `xml:"time,attr"` -} - -type Failure struct { - XMLName xml.Name `xml:"failure"` - Data string `xml:",chardata"` - Message string `xml:"message,attr"` - Type string `xml:"type,attr"` -} diff --git a/cli/detect/report/report.go b/cli/detect/report/report.go deleted file mode 100644 index 120841bb8..000000000 --- a/cli/detect/report/report.go +++ /dev/null @@ -1,38 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package report - -import ( - "io" -) - -const ( - // https://cwe.mitre.org/data/definitions/798.html - CWE = "CWE-798" - CWE_DESCRIPTION = "Use of Hard-coded Credentials" - StdoutReportPath = "-" -) - -type Reporter interface { - Write(w io.WriteCloser, findings []Finding) error -} diff --git a/cli/detect/report/sarif.go b/cli/detect/report/sarif.go deleted file mode 100644 index f7457eb57..000000000 --- a/cli/detect/report/sarif.go +++ /dev/null @@ -1,239 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package report - -import ( - "encoding/json" - "fmt" - "io" - - "github.com/Infisical/infisical-merge/detect/config" -) - -type SarifReporter struct { - OrderedRules []config.Rule -} - -var _ Reporter = (*SarifReporter)(nil) - -func (r *SarifReporter) Write(w io.WriteCloser, findings []Finding) error { - sarif := Sarif{ - Schema: "https://json.schemastore.org/sarif-2.1.0.json", - Version: "2.1.0", - Runs: r.getRuns(findings), - } - - encoder := json.NewEncoder(w) - encoder.SetIndent("", " ") - return encoder.Encode(sarif) -} - -func (r *SarifReporter) getRuns(findings []Finding) []Runs { - return []Runs{ - { - Tool: r.getTool(), - Results: getResults(findings), - }, - } -} - -func (r *SarifReporter) getTool() Tool { - tool := Tool{ - Driver: Driver{ - Name: driver, - SemanticVersion: version, - InformationUri: "https://github.com/gitleaks/gitleaks", - Rules: r.getRules(), - }, - } - - // if this tool has no rules, ensure that it is represented as [] instead of null/nil - if hasEmptyRules(tool) { - tool.Driver.Rules = make([]Rules, 0) - } - - return tool -} - -func hasEmptyRules(tool Tool) bool { - return len(tool.Driver.Rules) == 0 -} - -func (r *SarifReporter) getRules() []Rules { - // TODO for _, rule := range cfg.Rules { - var rules []Rules - for _, rule := range r.OrderedRules { - rules = append(rules, Rules{ - ID: rule.RuleID, - Description: ShortDescription{ - Text: rule.Description, - }, - }) - } - return rules -} - -func messageText(f Finding) string { - if f.Commit == "" { - return fmt.Sprintf("%s has detected secret for file %s.", f.RuleID, f.File) - } - - return fmt.Sprintf("%s has detected secret for file %s at commit %s.", f.RuleID, f.File, f.Commit) - -} - -func getResults(findings []Finding) []Results { - results := []Results{} - for _, f := range findings { - r := Results{ - Message: Message{ - Text: messageText(f), - }, - RuleId: f.RuleID, - Locations: getLocation(f), - // This information goes in partial fingerprings until revision - // data can be added somewhere else - PartialFingerPrints: PartialFingerPrints{ - CommitSha: f.Commit, - Email: f.Email, - CommitMessage: f.Message, - Date: f.Date, - Author: f.Author, - }, - Properties: Properties{ - Tags: f.Tags, - }, - } - results = append(results, r) - } - return results -} - -func getLocation(f Finding) []Locations { - uri := f.File - if f.SymlinkFile != "" { - uri = f.SymlinkFile - } - return []Locations{ - { - PhysicalLocation: PhysicalLocation{ - ArtifactLocation: ArtifactLocation{ - URI: uri, - }, - Region: Region{ - StartLine: f.StartLine, - EndLine: f.EndLine, - StartColumn: f.StartColumn, - EndColumn: f.EndColumn, - Snippet: Snippet{ - Text: f.Secret, - }, - }, - }, - }, - } -} - -type PartialFingerPrints struct { - CommitSha string `json:"commitSha"` - Email string `json:"email"` - Author string `json:"author"` - Date string `json:"date"` - CommitMessage string `json:"commitMessage"` -} - -type Sarif struct { - Schema string `json:"$schema"` - Version string `json:"version"` - Runs []Runs `json:"runs"` -} - -type ShortDescription struct { - Text string `json:"text"` -} - -type FullDescription struct { - Text string `json:"text"` -} - -type Rules struct { - ID string `json:"id"` - Description ShortDescription `json:"shortDescription"` -} - -type Driver struct { - Name string `json:"name"` - SemanticVersion string `json:"semanticVersion"` - InformationUri string `json:"informationUri"` - Rules []Rules `json:"rules"` -} - -type Tool struct { - Driver Driver `json:"driver"` -} - -type Message struct { - Text string `json:"text"` -} - -type ArtifactLocation struct { - URI string `json:"uri"` -} - -type Region struct { - StartLine int `json:"startLine"` - StartColumn int `json:"startColumn"` - EndLine int `json:"endLine"` - EndColumn int `json:"endColumn"` - Snippet Snippet `json:"snippet"` -} - -type Snippet struct { - Text string `json:"text"` -} - -type PhysicalLocation struct { - ArtifactLocation ArtifactLocation `json:"artifactLocation"` - Region Region `json:"region"` -} - -type Locations struct { - PhysicalLocation PhysicalLocation `json:"physicalLocation"` -} - -type Properties struct { - Tags []string `json:"tags"` -} - -type Results struct { - Message Message `json:"message"` - RuleId string `json:"ruleId"` - Locations []Locations `json:"locations"` - PartialFingerPrints `json:"partialFingerprints"` - Properties Properties `json:"properties"` -} - -type Runs struct { - Tool Tool `json:"tool"` - Results []Results `json:"results"` -} diff --git a/cli/detect/report/template.go b/cli/detect/report/template.go deleted file mode 100644 index 094aaaea9..000000000 --- a/cli/detect/report/template.go +++ /dev/null @@ -1,68 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package report - -import ( - "fmt" - "io" - "os" - "text/template" - - "github.com/Masterminds/sprig/v3" -) - -type TemplateReporter struct { - template *template.Template -} - -var _ Reporter = (*TemplateReporter)(nil) - -func NewTemplateReporter(templatePath string) (*TemplateReporter, error) { - if templatePath == "" { - return nil, fmt.Errorf("template path cannot be empty") - } - - file, err := os.ReadFile(templatePath) - if err != nil { - return nil, fmt.Errorf("error reading file: %w", err) - } - templateText := string(file) - - // TODO: Add helper functions like escaping for JSON, XML, etc. - t := template.New("custom") - t = t.Funcs(sprig.TxtFuncMap()) - t, err = t.Parse(templateText) - if err != nil { - return nil, fmt.Errorf("error parsing file: %w", err) - } - return &TemplateReporter{template: t}, nil -} - -// writeTemplate renders the findings using the user-provided template. -// https://www.digitalocean.com/community/tutorials/how-to-use-templates-in-go -func (t *TemplateReporter) Write(w io.WriteCloser, findings []Finding) error { - if err := t.template.Execute(w, findings); err != nil { - return err - } - return nil -} diff --git a/cli/detect/sources/directory.go b/cli/detect/sources/directory.go deleted file mode 100644 index 0ad46c3d8..000000000 --- a/cli/detect/sources/directory.go +++ /dev/null @@ -1,127 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package sources - -import ( - "io/fs" - "os" - "path/filepath" - "runtime" - - "github.com/fatih/semgroup" - - "github.com/Infisical/infisical-merge/detect/config" - "github.com/Infisical/infisical-merge/detect/logging" -) - -type ScanTarget struct { - Path string - Symlink string -} - -var isWindows = runtime.GOOS == "windows" - -func DirectoryTargets(source string, s *semgroup.Group, followSymlinks bool, allowlists []*config.Allowlist) (<-chan ScanTarget, error) { - paths := make(chan ScanTarget) - s.Go(func() error { - defer close(paths) - return filepath.Walk(source, - func(path string, fInfo os.FileInfo, err error) error { - logger := logging.With().Str("path", path).Logger() - - if err != nil { - if os.IsPermission(err) { - // This seems to only fail on directories at this stage. - logger.Warn().Msg("Skipping directory: permission denied") - return filepath.SkipDir - } - return err - } - - // Empty; nothing to do here. - if fInfo.Size() == 0 { - return nil - } - - // Unwrap symlinks, if |followSymlinks| is set. - scanTarget := ScanTarget{ - Path: path, - } - if fInfo.Mode().Type() == fs.ModeSymlink { - if !followSymlinks { - logger.Debug().Msg("Skipping symlink") - return nil - } - - realPath, err := filepath.EvalSymlinks(path) - if err != nil { - return err - } - - realPathFileInfo, _ := os.Stat(realPath) - if realPathFileInfo.IsDir() { - logger.Warn().Str("target", realPath).Msg("Skipping symlinked directory") - return nil - } - - scanTarget.Path = realPath - scanTarget.Symlink = path - } - - // TODO: Also run this check against the resolved symlink? - var skip bool - for _, a := range allowlists { - skip = a.PathAllowed(path) || - // TODO: Remove this in v9. - // This is an awkward hack to mitigate https://github.com/gitleaks/gitleaks/issues/1641. - (isWindows && a.PathAllowed(filepath.ToSlash(path))) - if skip { - break - } - } - if fInfo.IsDir() { - // Directory - if skip { - logger.Debug().Msg("Skipping directory due to global allowlist") - return filepath.SkipDir - } - - if fInfo.Name() == ".git" { - // Don't scan .git directories. - // TODO: Add this to the config allowlist, instead of hard-coding it. - return filepath.SkipDir - } - } else { - // File - if skip { - logger.Debug().Msg("Skipping file due to global allowlist") - return nil - } - - paths <- scanTarget - } - return nil - }) - }) - return paths, nil -} diff --git a/cli/detect/sources/git.go b/cli/detect/sources/git.go deleted file mode 100644 index 95b829a9a..000000000 --- a/cli/detect/sources/git.go +++ /dev/null @@ -1,211 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package sources - -import ( - "bufio" - "errors" - "io" - "os/exec" - "path/filepath" - "regexp" - "strings" - - "github.com/gitleaks/go-gitdiff/gitdiff" - - "github.com/Infisical/infisical-merge/detect/logging" -) - -var quotedOptPattern = regexp.MustCompile(`^(?:"[^"]+"|'[^']+')$`) - -// GitCmd helps to work with Git's output. -type GitCmd struct { - cmd *exec.Cmd - diffFilesCh <-chan *gitdiff.File - errCh <-chan error -} - -// NewGitLogCmd returns `*DiffFilesCmd` with two channels: `<-chan *gitdiff.File` and `<-chan error`. -// Caller should read everything from channels until receiving a signal about their closure and call -// the `func (*DiffFilesCmd) Wait()` error in order to release resources. -func NewGitLogCmd(source string, logOpts string) (*GitCmd, error) { - sourceClean := filepath.Clean(source) - var cmd *exec.Cmd - if logOpts != "" { - args := []string{"-C", sourceClean, "log", "-p", "-U0"} - - // Ensure that the user-provided |logOpts| aren't wrapped in quotes. - // https://github.com/gitleaks/gitleaks/issues/1153 - userArgs := strings.Split(logOpts, " ") - var quotedOpts []string - for _, element := range userArgs { - if quotedOptPattern.MatchString(element) { - quotedOpts = append(quotedOpts, element) - } - } - if len(quotedOpts) > 0 { - logging.Warn().Msgf("the following `--log-opts` values may not work as expected: %v\n\tsee https://github.com/gitleaks/gitleaks/issues/1153 for more information", quotedOpts) - } - - args = append(args, userArgs...) - cmd = exec.Command("git", args...) - } else { - cmd = exec.Command("git", "-C", sourceClean, "log", "-p", "-U0", - "--full-history", "--all") - } - - logging.Debug().Msgf("executing: %s", cmd.String()) - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, err - } - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, err - } - if err := cmd.Start(); err != nil { - return nil, err - } - - errCh := make(chan error) - go listenForStdErr(stderr, errCh) - - gitdiffFiles, err := gitdiff.Parse(stdout) - if err != nil { - return nil, err - } - - return &GitCmd{ - cmd: cmd, - diffFilesCh: gitdiffFiles, - errCh: errCh, - }, nil -} - -// NewGitDiffCmd returns `*DiffFilesCmd` with two channels: `<-chan *gitdiff.File` and `<-chan error`. -// Caller should read everything from channels until receiving a signal about their closure and call -// the `func (*DiffFilesCmd) Wait()` error in order to release resources. -func NewGitDiffCmd(source string, staged bool) (*GitCmd, error) { - sourceClean := filepath.Clean(source) - var cmd *exec.Cmd - cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", "--no-ext-diff", ".") - if staged { - cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", "--no-ext-diff", - "--staged", ".") - } - logging.Debug().Msgf("executing: %s", cmd.String()) - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, err - } - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, err - } - if err := cmd.Start(); err != nil { - return nil, err - } - - errCh := make(chan error) - go listenForStdErr(stderr, errCh) - - gitdiffFiles, err := gitdiff.Parse(stdout) - if err != nil { - return nil, err - } - - return &GitCmd{ - cmd: cmd, - diffFilesCh: gitdiffFiles, - errCh: errCh, - }, nil -} - -// DiffFilesCh returns a channel with *gitdiff.File. -func (c *GitCmd) DiffFilesCh() <-chan *gitdiff.File { - return c.diffFilesCh -} - -// ErrCh returns a channel that could produce an error if there is something in stderr. -func (c *GitCmd) ErrCh() <-chan error { - return c.errCh -} - -// Wait waits for the command to exit and waits for any copying to -// stdin or copying from stdout or stderr to complete. -// -// Wait also closes underlying stdout and stderr. -func (c *GitCmd) Wait() (err error) { - return c.cmd.Wait() -} - -// listenForStdErr listens for stderr output from git, prints it to stdout, -// sends to errCh and closes it. -func listenForStdErr(stderr io.ReadCloser, errCh chan<- error) { - defer close(errCh) - - var errEncountered bool - - scanner := bufio.NewScanner(stderr) - for scanner.Scan() { - // if git throws one of the following errors: - // - // exhaustive rename detection was skipped due to too many files. - // you may want to set your diff.renameLimit variable to at least - // (some large number) and retry the command. - // - // inexact rename detection was skipped due to too many files. - // you may want to set your diff.renameLimit variable to at least - // (some large number) and retry the command. - // - // Auto packing the repository in background for optimum performance. - // See "git help gc" for manual housekeeping. - // - // we skip exiting the program as git log -p/git diff will continue - // to send data to stdout and finish executing. This next bit of - // code prevents gitleaks from stopping mid scan if this error is - // encountered - if strings.Contains(scanner.Text(), - "exhaustive rename detection was skipped") || - strings.Contains(scanner.Text(), - "inexact rename detection was skipped") || - strings.Contains(scanner.Text(), - "you may want to set your diff.renameLimit") || - strings.Contains(scanner.Text(), - "See \"git help gc\" for manual housekeeping") || - strings.Contains(scanner.Text(), - "Auto packing the repository in background for optimum performance") { - logging.Warn().Msg(scanner.Text()) - } else { - logging.Error().Msgf("[git] %s", scanner.Text()) - errEncountered = true - } - } - - if errEncountered { - errCh <- errors.New("stderr is not empty") - return - } -} diff --git a/cli/detect/utils.go b/cli/detect/utils.go deleted file mode 100644 index 84b1017fc..000000000 --- a/cli/detect/utils.go +++ /dev/null @@ -1,280 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package detect - -import ( - // "encoding/json" - "fmt" - "math" - "path/filepath" - "strings" - "time" - - "github.com/Infisical/infisical-merge/detect/cmd/scm" - "github.com/Infisical/infisical-merge/detect/logging" - "github.com/Infisical/infisical-merge/detect/report" - - "github.com/charmbracelet/lipgloss" - "github.com/gitleaks/go-gitdiff/gitdiff" -) - -// augmentGitFinding updates the start and end line numbers of a finding to include the -// delta from the git diff -func augmentGitFinding(remote *RemoteInfo, finding report.Finding, textFragment *gitdiff.TextFragment, f *gitdiff.File) report.Finding { - if !strings.HasPrefix(finding.Match, "file detected") { - finding.StartLine += int(textFragment.NewPosition) - finding.EndLine += int(textFragment.NewPosition) - } - - if f.PatchHeader != nil { - finding.Commit = f.PatchHeader.SHA - if f.PatchHeader.Author != nil { - finding.Author = f.PatchHeader.Author.Name - finding.Email = f.PatchHeader.Author.Email - } - finding.Date = f.PatchHeader.AuthorDate.UTC().Format(time.RFC3339) - finding.Message = f.PatchHeader.Message() - // Results from `git diff` shouldn't have a link. - if finding.Commit != "" { - finding.Link = createScmLink(remote.Platform, remote.Url, finding) - } - } - return finding -} - -var linkCleaner = strings.NewReplacer( - " ", "%20", - "%", "%25", -) - -func createScmLink(scmPlatform scm.Platform, remoteUrl string, finding report.Finding) string { - if scmPlatform == scm.UnknownPlatform || scmPlatform == scm.NoPlatform { - return "" - } - - // Clean the path. - var ( - filePath = linkCleaner.Replace(finding.File) - ext = strings.ToLower(filepath.Ext(filePath)) - ) - - switch scmPlatform { - case scm.GitHubPlatform: - link := fmt.Sprintf("%s/blob/%s/%s", remoteUrl, finding.Commit, filePath) - if ext == ".ipynb" || ext == ".md" { - link += "?plain=1" - } - if finding.StartLine != 0 { - link += fmt.Sprintf("#L%d", finding.StartLine) - } - if finding.EndLine != finding.StartLine { - link += fmt.Sprintf("-L%d", finding.EndLine) - } - return link - case scm.GitLabPlatform: - link := fmt.Sprintf("%s/blob/%s/%s", remoteUrl, finding.Commit, filePath) - if finding.StartLine != 0 { - link += fmt.Sprintf("#L%d", finding.StartLine) - } - if finding.EndLine != finding.StartLine { - link += fmt.Sprintf("-%d", finding.EndLine) - } - return link - case scm.AzureDevOpsPlatform: - link := fmt.Sprintf("%s/commit/%s?path=/%s", remoteUrl, finding.Commit, filePath) - // Add line information if applicable - if finding.StartLine != 0 { - link += fmt.Sprintf("&line=%d", finding.StartLine) - } - if finding.EndLine != finding.StartLine { - link += fmt.Sprintf("&lineEnd=%d", finding.EndLine) - } - // This is a bit dirty, but Azure DevOps does not highlight the line when the lineStartColumn and lineEndColumn are not provided - link += "&lineStartColumn=1&lineEndColumn=10000000&type=2&lineStyle=plain&_a=files" - return link - case scm.BitBucketPlatform: - link := fmt.Sprintf("%s/src/%s/%s", remoteUrl, finding.Commit, filePath) - if finding.StartLine != 0 { - link += fmt.Sprintf("#lines-%d", finding.StartLine) - } - if finding.EndLine != finding.StartLine { - link += fmt.Sprintf(":%d", finding.EndLine) - } - return link - default: - // This should never happen. - return "" - } -} - -// shannonEntropy calculates the entropy of data using the formula defined here: -// https://en.wiktionary.org/wiki/Shannon_entropy -// Another way to think about what this is doing is calculating the number of bits -// needed to on average encode the data. So, the higher the entropy, the more random the data, the -// more bits needed to encode that data. -func shannonEntropy(data string) (entropy float64) { - if data == "" { - return 0 - } - - charCounts := make(map[rune]int) - for _, char := range data { - charCounts[char]++ - } - - invLength := 1.0 / float64(len(data)) - for _, count := range charCounts { - freq := float64(count) * invLength - entropy -= freq * math.Log2(freq) - } - - return entropy -} - -// filter will dedupe and redact findings -func filter(findings []report.Finding, redact uint) []report.Finding { - var retFindings []report.Finding - for _, f := range findings { - include := true - if strings.Contains(strings.ToLower(f.RuleID), "generic") { - for _, fPrime := range findings { - if f.StartLine == fPrime.StartLine && - f.Commit == fPrime.Commit && - f.RuleID != fPrime.RuleID && - strings.Contains(fPrime.Secret, f.Secret) && - !strings.Contains(strings.ToLower(fPrime.RuleID), "generic") { - - genericMatch := strings.Replace(f.Match, f.Secret, "REDACTED", -1) - betterMatch := strings.Replace(fPrime.Match, fPrime.Secret, "REDACTED", -1) - logging.Trace().Msgf("skipping %s finding (%s), %s rule takes precedence (%s)", f.RuleID, genericMatch, fPrime.RuleID, betterMatch) - include = false - break - } - } - } - - if redact > 0 { - f.Redact(redact) - } - if include { - retFindings = append(retFindings, f) - } - } - return retFindings -} - -func printFinding(f report.Finding, noColor bool) { - // trim all whitespace and tabs - f.Line = strings.TrimSpace(f.Line) - f.Secret = strings.TrimSpace(f.Secret) - f.Match = strings.TrimSpace(f.Match) - - isFileMatch := strings.HasPrefix(f.Match, "file detected:") - skipColor := noColor - finding := "" - var secret lipgloss.Style - - // Matches from filenames do not have a |line| or |secret| - if !isFileMatch { - matchInLineIDX := strings.Index(f.Line, f.Match) - secretInMatchIdx := strings.Index(f.Match, f.Secret) - - skipColor = false - - if matchInLineIDX == -1 || noColor { - skipColor = true - matchInLineIDX = 0 - } - - start := f.Line[0:matchInLineIDX] - startMatchIdx := 0 - if matchInLineIDX > 20 { - startMatchIdx = matchInLineIDX - 20 - start = "..." + f.Line[startMatchIdx:matchInLineIDX] - } - - matchBeginning := lipgloss.NewStyle().SetString(f.Match[0:secretInMatchIdx]).Foreground(lipgloss.Color("#f5d445")) - secret = lipgloss.NewStyle().SetString(f.Secret). - Bold(true). - Italic(true). - Foreground(lipgloss.Color("#f05c07")) - matchEnd := lipgloss.NewStyle().SetString(f.Match[secretInMatchIdx+len(f.Secret):]).Foreground(lipgloss.Color("#f5d445")) - - lineEndIdx := matchInLineIDX + len(f.Match) - if len(f.Line)-1 <= lineEndIdx { - lineEndIdx = len(f.Line) - } - - lineEnd := f.Line[lineEndIdx:] - - if len(f.Secret) > 100 { - secret = lipgloss.NewStyle().SetString(f.Secret[0:100] + "..."). - Bold(true). - Italic(true). - Foreground(lipgloss.Color("#f05c07")) - } - if len(lineEnd) > 20 { - lineEnd = lineEnd[0:20] + "..." - } - - finding = fmt.Sprintf("%s%s%s%s%s\n", strings.TrimPrefix(strings.TrimLeft(start, " "), "\n"), matchBeginning, secret, matchEnd, lineEnd) - } - - if skipColor || isFileMatch { - fmt.Printf("%-12s %s\n", "Finding:", f.Match) - fmt.Printf("%-12s %s\n", "Secret:", f.Secret) - } else { - fmt.Printf("%-12s %s", "Finding:", finding) - fmt.Printf("%-12s %s\n", "Secret:", secret) - } - - fmt.Printf("%-12s %s\n", "RuleID:", f.RuleID) - fmt.Printf("%-12s %f\n", "Entropy:", f.Entropy) - if f.File == "" { - fmt.Println("") - return - } - if len(f.Tags) > 0 { - fmt.Printf("%-12s %s\n", "Tags:", f.Tags) - } - fmt.Printf("%-12s %s\n", "File:", f.File) - fmt.Printf("%-12s %d\n", "Line:", f.StartLine) - if f.Commit == "" { - fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint) - fmt.Println("") - return - } - fmt.Printf("%-12s %s\n", "Commit:", f.Commit) - fmt.Printf("%-12s %s\n", "Author:", f.Author) - fmt.Printf("%-12s %s\n", "Email:", f.Email) - fmt.Printf("%-12s %s\n", "Date:", f.Date) - fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint) - if f.Link != "" { - fmt.Printf("%-12s %s\n", "Link:", f.Link) - } - fmt.Println("") -} - -func isWhitespace(ch byte) bool { - return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' -} diff --git a/cli/docker/alpine b/cli/docker/alpine deleted file mode 100644 index e1a59d6eb..000000000 --- a/cli/docker/alpine +++ /dev/null @@ -1,9 +0,0 @@ -FROM alpine -RUN apk add --no-cache tini - -## Upgrade OpenSSL libraries to mitigate known vulnerabilities as the current Alpine image has not been patched yet. -RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 - - -COPY infisical /bin/infisical -ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"] \ No newline at end of file diff --git a/cli/go.mod b/cli/go.mod deleted file mode 100644 index 3afc8d3be..000000000 --- a/cli/go.mod +++ /dev/null @@ -1,183 +0,0 @@ -module github.com/Infisical/infisical-merge - -go 1.23.0 - -toolchain go1.23.5 - -require ( - github.com/BobuSumisu/aho-corasick v1.0.3 - github.com/Masterminds/sprig/v3 v3.3.0 - github.com/bradleyjkemp/cupaloy/v2 v2.8.0 - github.com/charmbracelet/lipgloss v0.9.1 - github.com/creack/pty v1.1.21 - github.com/denisbrodbeck/machineid v1.0.1 - github.com/fatih/semgroup v1.2.0 - github.com/gitleaks/go-gitdiff v0.9.1 - github.com/h2non/filetype v1.1.3 - github.com/infisical/go-sdk v0.5.96 - github.com/infisical/infisical-kmip v0.3.5 - github.com/mattn/go-isatty v0.0.20 - github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a - github.com/muesli/mango-cobra v1.2.0 - github.com/muesli/reflow v0.3.0 - github.com/muesli/roff v0.1.0 - github.com/pion/dtls/v3 v3.0.4 - github.com/pion/logging v0.2.3 - github.com/pion/turn/v4 v4.0.0 - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/pkg/errors v0.9.1 - github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a - github.com/quic-go/quic-go v0.50.0 - github.com/rs/cors v1.11.0 - github.com/rs/zerolog v1.26.1 - github.com/spf13/cobra v1.6.1 - github.com/spf13/viper v1.8.1 - github.com/stretchr/testify v1.10.0 - github.com/wasilibs/go-re2 v1.10.0 - golang.org/x/crypto v0.36.0 - golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 - golang.org/x/sys v0.31.0 - golang.org/x/term v0.30.0 - gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.31.4 - k8s.io/apimachinery v0.31.4 - k8s.io/client-go v0.31.4 -) - -require ( - cloud.google.com/go/auth v0.7.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - cloud.google.com/go/compute/metadata v0.4.0 // indirect - cloud.google.com/go/iam v1.1.11 // indirect - dario.cat/mergo v1.0.1 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect - github.com/alessio/shellescape v1.4.1 // indirect - github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect - github.com/aws/aws-sdk-go-v2 v1.27.2 // indirect - github.com/aws/aws-sdk-go-v2/config v1.27.18 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.18 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect - github.com/aws/smithy-go v1.20.2 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/chzyer/readline v1.5.1 // indirect - github.com/danieljoos/wincred v1.2.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dvsekhvalnov/jose2go v1.6.0 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/errors v0.20.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/strfmt v0.21.3 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.5 // indirect - github.com/gosimple/slug v1.15.0 // indirect - github.com/gosimple/unidecode v1.0.1 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/huandu/xstrings v1.5.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.5 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mtibben/percent v0.2.1 // indirect - github.com/muesli/mango v0.1.0 // indirect - github.com/muesli/mango-pflag v0.1.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oklog/ulid v1.3.1 // indirect - github.com/onsi/ginkgo/v2 v2.22.2 // indirect - github.com/pelletier/go-toml v1.9.3 // indirect - github.com/pion/randutil v0.1.0 // indirect - github.com/pion/stun/v3 v3.0.0 // indirect - github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/spf13/afero v1.6.0 // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/subosito/gotenv v1.2.0 // indirect - github.com/tetratelabs/wazero v1.9.0 // indirect - github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect - github.com/wlynxg/anet v0.0.5 // indirect - github.com/x448/float16 v0.8.4 // indirect - github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect - go.mongodb.org/mongo-driver v1.10.0 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.uber.org/mock v0.5.0 // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.30.0 // indirect - google.golang.org/api v0.188.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect - google.golang.org/grpc v1.64.1 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.62.0 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect -) - -require ( - github.com/fatih/color v1.17.0 - github.com/go-resty/resty/v2 v2.16.5 - github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/jedib0t/go-pretty v4.3.0+incompatible - github.com/manifoldco/promptui v0.9.0 - github.com/spf13/pflag v1.0.5 // indirect - github.com/zalando/go-keyring v0.2.3 -) - -replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.2 - -replace github.com/pion/turn/v4 => github.com/Infisical/turn/v4 v4.0.1 diff --git a/cli/go.sum b/cli/go.sum deleted file mode 100644 index 066f736a2..000000000 --- a/cli/go.sum +++ /dev/null @@ -1,951 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts= -cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c= -cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw= -cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= -github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8= -github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto= -github.com/Infisical/turn/v4 v4.0.1 h1:omdelNsnFfzS5cu86W5OBR68by68a8sva4ogR0lQQnw= -github.com/Infisical/turn/v4 v4.0.1/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8= -github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= -github.com/aws/aws-sdk-go-v2/config v1.27.18 h1:wFvAnwOKKe7QAyIxziwSKjmer9JBMH1vzIL6W+fYuKk= -github.com/aws/aws-sdk-go-v2/config v1.27.18/go.mod h1:0xz6cgdX55+kmppvPm2IaKzIXOheGJhAufacPJaXZ7c= -github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9aYTT5STS/CvnkqY1c= -github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 h1:o4T+fKxA3gTMcluBNZZXE9DNaMkJuUL1O3mffCUjoJo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11/go.mod h1:84oZdJ+VjuJKs9v1UTC9NaodRZRseOXCTgku+vQJWR8= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 h1:gEYM2GSpr4YNWc6hCd5nod4+d4kd9vWIAWrmGuLdlMw= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.11/go.mod h1:gVvwPdPNYehHSP9Rs7q27U1EU+3Or2ZpXvzAYJNh63w= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 h1:iXjh3uaH3vsVcnyZX7MqCoCfcyxIrVE9iOQruRaWPrQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5/go.mod h1:5ZXesEuy/QcO0WUnt+4sDkxhdXRHTu2yG0uCSH8B6os= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF5e2mfxHCg7ZVMYmk= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk= -github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= -github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= -github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= -github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= -github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= -github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= -github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= -github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/fatih/semgroup v1.2.0 h1:h/OLXwEM+3NNyAdZEpMiH1OzfplU09i2qXPVThGZvyg= -github.com/fatih/semgroup v1.2.0/go.mod h1:1KAD4iIYfXjE4U13B48VM4z9QUwV5Tt8O4rS879kgm8= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0= -github.com/gitleaks/go-gitdiff v0.9.1/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= -github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= -github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= -github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= -github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= -github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= -github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= -github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= -github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= -github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/infisical/go-sdk v0.5.96 h1:huky6bQ1Y3oRdPb5MO3Ru868qZaPHUxZ7kP7FPNRn48= -github.com/infisical/go-sdk v0.5.96/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= -github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE= -github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= -github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= -github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= -github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= -github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= -github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4= -github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= -github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= -github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= -github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= -github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= -github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= -github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= -github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= -github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg= -github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= -github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= -github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= -github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= -github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= -github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= -github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a h1:Ey0XWvrg6u6hyIn1Kd/jCCmL+bMv9El81tvuGBbxZGg= -github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= -github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= -github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= -github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/wasilibs/go-re2 v1.10.0 h1:vQZEBYZOCA9jdBMmrO4+CvqyCj0x4OomXTJ4a5/urQ0= -github.com/wasilibs/go-re2 v1.10.0/go.mod h1:k+5XqO2bCJS+QpGOnqugyfwC04nw0jaglmjrrkG8U6o= -github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= -github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= -github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= -github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= -github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= -github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= -go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c= -golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw= -google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.31.4 h1:I2QNzitPVsPeLQvexMEsj945QumYraqv9m74isPDKhM= -k8s.io/api v0.31.4/go.mod h1:d+7vgXLvmcdT1BCo79VEgJxHHryww3V5np2OYTr6jdw= -k8s.io/apimachinery v0.31.4 h1:8xjE2C4CzhYVm9DGf60yohpNUh5AEBnPxCryPBECmlM= -k8s.io/apimachinery v0.31.4/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/client-go v0.31.4 h1:t4QEXt4jgHIkKKlx06+W3+1JOwAFU/2OPiOo7H92eRQ= -k8s.io/client-go v0.31.4/go.mod h1:kvuMro4sFYIa8sulL5Gi5GFqUPvfH2O/dXuKstbaaeg= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/cli/goreleaser.dockerfile b/cli/goreleaser.dockerfile deleted file mode 100644 index 0436d4d8e..000000000 --- a/cli/goreleaser.dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM alpine -RUN apk add --no-cache tini -COPY infisical /bin/infisical -ENTRYPOINT ["/sbin/tini", "--", "/bin/infisical"] \ No newline at end of file diff --git a/cli/infisical-cli.repo b/cli/infisical-cli.repo deleted file mode 100644 index 74c39daca..000000000 --- a/cli/infisical-cli.repo +++ /dev/null @@ -1,5 +0,0 @@ -[infisical] -name=Infisical CLI -baseurl=https://yum.fury.io/infisical/ -enabled=1 -gpgcheck=0 \ No newline at end of file diff --git a/cli/main.go b/cli/main.go deleted file mode 100644 index 75152ffc4..000000000 --- a/cli/main.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package main - -import ( - "os" - - "github.com/Infisical/infisical-merge/packages/cmd" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -func main() { - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) - cmd.Execute() -} diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go deleted file mode 100644 index 15f75a57d..000000000 --- a/cli/packages/api/api.go +++ /dev/null @@ -1,652 +0,0 @@ -package api - -import ( - "fmt" - "net/http" - "strings" - - "github.com/Infisical/infisical-merge/packages/config" - "github.com/go-resty/resty/v2" - "github.com/rs/zerolog/log" -) - -const USER_AGENT = "cli" - -const ( - operationCallGetRawSecretsV3 = "CallGetRawSecretsV3" - operationCallGetEncryptedWorkspaceKey = "CallGetEncryptedWorkspaceKey" - operationCallGetServiceTokenDetails = "CallGetServiceTokenDetails" - operationCallLogin1V3 = "CallLogin1V3" - operationCallVerifyMfaToken = "CallVerifyMfaToken" - operationCallLogin2V3 = "CallLogin2V3" - operationCallGetAllOrganizations = "CallGetAllOrganizations" - operationCallSelectOrganization = "CallSelectOrganization" - operationCallGetAllWorkSpacesUserBelongsTo = "CallGetAllWorkSpacesUserBelongsTo" - operationCallGetProjectById = "CallGetProjectById" - operationCallIsAuthenticated = "CallIsAuthenticated" - operationCallGetNewAccessTokenWithRefreshToken = "CallGetNewAccessTokenWithRefreshToken" - operationCallGetFoldersV1 = "CallGetFoldersV1" - operationCallCreateFolderV1 = "CallCreateFolderV1" - operationCallDeleteFolderV1 = "CallDeleteFolderV1" - operationCallDeleteSecretsV3 = "CallDeleteSecretsV3" - operationCallCreateServiceToken = "CallCreateServiceToken" - operationCallUniversalAuthLogin = "CallUniversalAuthLogin" - operationCallMachineIdentityRefreshAccessToken = "CallMachineIdentityRefreshAccessToken" - operationCallFetchSingleSecretByName = "CallFetchSingleSecretByName" - operationCallCreateRawSecretsV3 = "CallCreateRawSecretsV3" - operationCallUpdateRawSecretsV3 = "CallUpdateRawSecretsV3" - operationCallRegisterGatewayIdentityV1 = "CallRegisterGatewayIdentityV1" - operationCallExchangeRelayCertV1 = "CallExchangeRelayCertV1" - operationCallGatewayHeartBeatV1 = "CallGatewayHeartBeatV1" - operationCallBootstrapInstance = "CallBootstrapInstance" -) - -func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) { - endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId) - var result GetEncryptedWorkspaceKeyResponse - response, err := httpClient. - R(). - SetResult(&result). - SetHeader("User-Agent", USER_AGENT). - Get(endpoint) - - if err != nil { - return GetEncryptedWorkspaceKeyResponse{}, NewGenericRequestError(operationCallGetEncryptedWorkspaceKey, err) - } - - if response.IsError() { - return GetEncryptedWorkspaceKeyResponse{}, NewAPIErrorWithResponse(operationCallGetEncryptedWorkspaceKey, response, nil) - } - - return result, nil -} - -func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDetailsResponse, error) { - var tokenDetailsResponse GetServiceTokenDetailsResponse - response, err := httpClient. - R(). - SetResult(&tokenDetailsResponse). - SetHeader("User-Agent", USER_AGENT). - Get(fmt.Sprintf("%v/v2/service-token", config.INFISICAL_URL)) - - if err != nil { - return GetServiceTokenDetailsResponse{}, NewGenericRequestError(operationCallGetServiceTokenDetails, err) - } - - if response.IsError() { - return GetServiceTokenDetailsResponse{}, NewAPIErrorWithResponse(operationCallGetServiceTokenDetails, response, nil) - } - - return tokenDetailsResponse, nil -} - -func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) { - var loginOneV2Response GetLoginOneV2Response - response, err := httpClient. - R(). - SetResult(&loginOneV2Response). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v3/auth/login1", config.INFISICAL_URL)) - - if err != nil { - return GetLoginOneV2Response{}, NewGenericRequestError(operationCallLogin1V3, err) - } - - if response.IsError() { - return GetLoginOneV2Response{}, NewAPIErrorWithResponse(operationCallLogin1V3, response, nil) - } - - return loginOneV2Response, nil -} - -func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest) (*VerifyMfaTokenResponse, *VerifyMfaTokenErrorResponse, error) { - var verifyMfaTokenResponse VerifyMfaTokenResponse - var responseError VerifyMfaTokenErrorResponse - response, err := httpClient. - R(). - SetResult(&verifyMfaTokenResponse). - SetHeader("User-Agent", USER_AGENT). - SetError(&responseError). - SetBody(request). - Post(fmt.Sprintf("%v/v2/auth/mfa/verify", config.INFISICAL_URL)) - - cookies := response.Cookies() - // Find a cookie by name - cookieName := "jid" - var refreshToken *http.Cookie - for _, cookie := range cookies { - if cookie.Name == cookieName { - refreshToken = cookie - break - } - } - - // When MFA is enabled - if refreshToken != nil { - verifyMfaTokenResponse.RefreshToken = refreshToken.Value - } - - if err != nil { - return nil, nil, NewGenericRequestError(operationCallVerifyMfaToken, err) - } - - if response.IsError() { - return nil, &responseError, nil - } - - return &verifyMfaTokenResponse, nil, nil -} - -func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLoginTwoV2Response, error) { - var loginTwoV2Response GetLoginTwoV2Response - response, err := httpClient. - R(). - SetResult(&loginTwoV2Response). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v3/auth/login2", config.INFISICAL_URL)) - - cookies := response.Cookies() - // Find a cookie by name - cookieName := "jid" - var refreshToken *http.Cookie - for _, cookie := range cookies { - if cookie.Name == cookieName { - refreshToken = cookie - break - } - } - - // When MFA is enabled - if refreshToken != nil { - loginTwoV2Response.RefreshToken = refreshToken.Value - } - - if err != nil { - return GetLoginTwoV2Response{}, NewGenericRequestError(operationCallLogin2V3, err) - } - - if response.IsError() { - return GetLoginTwoV2Response{}, NewAPIErrorWithResponse(operationCallLogin2V3, response, nil) - } - - return loginTwoV2Response, nil -} - -func CallGetAllOrganizations(httpClient *resty.Client) (GetOrganizationsResponse, error) { - var orgResponse GetOrganizationsResponse - response, err := httpClient. - R(). - SetResult(&orgResponse). - SetHeader("User-Agent", USER_AGENT). - Get(fmt.Sprintf("%v/v1/organization", config.INFISICAL_URL)) - - if err != nil { - return GetOrganizationsResponse{}, NewGenericRequestError(operationCallGetAllOrganizations, err) - } - - if response.IsError() { - return GetOrganizationsResponse{}, NewAPIErrorWithResponse(operationCallGetAllOrganizations, response, nil) - } - - return orgResponse, nil -} - -func CallSelectOrganization(httpClient *resty.Client, request SelectOrganizationRequest) (SelectOrganizationResponse, error) { - var selectOrgResponse SelectOrganizationResponse - - response, err := httpClient. - R(). - SetBody(request). - SetResult(&selectOrgResponse). - SetHeader("User-Agent", USER_AGENT). - Post(fmt.Sprintf("%v/v3/auth/select-organization", config.INFISICAL_URL)) - - if err != nil { - return SelectOrganizationResponse{}, NewGenericRequestError(operationCallSelectOrganization, err) - } - - if response.IsError() { - return SelectOrganizationResponse{}, NewAPIErrorWithResponse(operationCallSelectOrganization, response, nil) - } - - return selectOrgResponse, nil - -} - -func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesResponse, error) { - var workSpacesResponse GetWorkSpacesResponse - response, err := httpClient. - R(). - SetResult(&workSpacesResponse). - SetHeader("User-Agent", USER_AGENT). - Get(fmt.Sprintf("%v/v1/workspace", config.INFISICAL_URL)) - - if err != nil { - return GetWorkSpacesResponse{}, err - } - - if response.IsError() { - return GetWorkSpacesResponse{}, fmt.Errorf("CallGetAllWorkSpacesUserBelongsTo: Unsuccessful response: [response=%v]", response) - } - - return workSpacesResponse, nil -} - -func CallGetProjectById(httpClient *resty.Client, id string) (Project, error) { - var projectResponse GetProjectByIdResponse - response, err := httpClient. - R(). - SetResult(&projectResponse). - SetHeader("User-Agent", USER_AGENT). - Get(fmt.Sprintf("%v/v1/workspace/%s", config.INFISICAL_URL, id)) - - if err != nil { - return Project{}, NewGenericRequestError(operationCallGetProjectById, err) - } - - if response.IsError() { - return Project{}, NewAPIErrorWithResponse(operationCallGetProjectById, response, nil) - } - - return projectResponse.Project, nil -} - -func CallIsAuthenticated(httpClient *resty.Client) bool { - var workSpacesResponse GetWorkSpacesResponse - response, err := httpClient. - R(). - SetResult(&workSpacesResponse). - SetHeader("User-Agent", USER_AGENT). - Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL)) - - if err != nil { - return false - } - - if response.IsError() { - log.Debug().Msgf("%s: Unsuccessful response: [response=%v]", operationCallIsAuthenticated, response) - return false - } - - return true -} - -func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToken string) (GetNewAccessTokenWithRefreshTokenResponse, error) { - var newAccessToken GetNewAccessTokenWithRefreshTokenResponse - response, err := httpClient. - R(). - SetResult(&newAccessToken). - SetHeader("User-Agent", USER_AGENT). - SetCookie(&http.Cookie{ - Name: "jid", - Value: refreshToken, - }). - Post(fmt.Sprintf("%v/v1/auth/token", config.INFISICAL_URL)) - - if err != nil { - return GetNewAccessTokenWithRefreshTokenResponse{}, NewGenericRequestError(operationCallGetNewAccessTokenWithRefreshToken, err) - } - - if response.IsError() { - return GetNewAccessTokenWithRefreshTokenResponse{}, NewAPIErrorWithResponse(operationCallGetNewAccessTokenWithRefreshToken, response, nil) - } - - return newAccessToken, nil -} - -func CallGetFoldersV1(httpClient *resty.Client, request GetFoldersV1Request) (GetFoldersV1Response, error) { - var foldersResponse GetFoldersV1Response - httpRequest := httpClient. - R(). - SetResult(&foldersResponse). - SetHeader("User-Agent", USER_AGENT). - SetQueryParam("environment", request.Environment). - SetQueryParam("workspaceId", request.WorkspaceId). - SetQueryParam("directory", request.FoldersPath) - - response, err := httpRequest.Get(fmt.Sprintf("%v/v1/folders", config.INFISICAL_URL)) - - if err != nil { - return GetFoldersV1Response{}, NewGenericRequestError(operationCallGetFoldersV1, err) - } - - if response.IsError() { - return GetFoldersV1Response{}, NewAPIErrorWithResponse(operationCallGetFoldersV1, response, nil) - } - - return foldersResponse, nil -} - -func CallCreateFolderV1(httpClient *resty.Client, request CreateFolderV1Request) (CreateFolderV1Response, error) { - var folderResponse CreateFolderV1Response - httpRequest := httpClient. - R(). - SetResult(&folderResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request) - - response, err := httpRequest.Post(fmt.Sprintf("%v/v1/folders", config.INFISICAL_URL)) - if err != nil { - return CreateFolderV1Response{}, NewGenericRequestError(operationCallCreateFolderV1, err) - } - - if response.IsError() { - return CreateFolderV1Response{}, NewAPIErrorWithResponse(operationCallCreateFolderV1, response, nil) - } - - return folderResponse, nil -} - -func CallDeleteFolderV1(httpClient *resty.Client, request DeleteFolderV1Request) (DeleteFolderV1Response, error) { - var folderResponse DeleteFolderV1Response - - httpRequest := httpClient. - R(). - SetResult(&folderResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request) - - response, err := httpRequest.Delete(fmt.Sprintf("%v/v1/folders/%v", config.INFISICAL_URL, request.FolderName)) - if err != nil { - return DeleteFolderV1Response{}, NewGenericRequestError(operationCallDeleteFolderV1, err) - } - - if response.IsError() { - return DeleteFolderV1Response{}, NewAPIErrorWithResponse(operationCallDeleteFolderV1, response, nil) - } - - return folderResponse, nil -} - -func CallDeleteSecretsRawV3(httpClient *resty.Client, request DeleteSecretV3Request) error { - - var secretsResponse GetEncryptedSecretsV3Response - response, err := httpClient. - R(). - SetResult(&secretsResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Delete(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) - - if err != nil { - return NewGenericRequestError(operationCallDeleteSecretsV3, err) - } - - if response.IsError() { - additionalContext := "Please make sure your secret path, workspace and environment name are all correct." - return NewAPIErrorWithResponse(operationCallDeleteSecretsV3, response, &additionalContext) - } - - return nil -} - -func CallCreateServiceToken(httpClient *resty.Client, request CreateServiceTokenRequest) (CreateServiceTokenResponse, error) { - var createServiceTokenResponse CreateServiceTokenResponse - response, err := httpClient. - R(). - SetResult(&createServiceTokenResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v2/service-token/", config.INFISICAL_URL)) - - if err != nil { - return CreateServiceTokenResponse{}, NewGenericRequestError(operationCallCreateServiceToken, err) - } - - if response.IsError() { - return CreateServiceTokenResponse{}, NewAPIErrorWithResponse(operationCallCreateServiceToken, response, nil) - } - - return createServiceTokenResponse, nil -} - -func CallUniversalAuthLogin(httpClient *resty.Client, request UniversalAuthLoginRequest) (UniversalAuthLoginResponse, error) { - var universalAuthLoginResponse UniversalAuthLoginResponse - response, err := httpClient. - R(). - SetResult(&universalAuthLoginResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v1/auth/universal-auth/login/", config.INFISICAL_URL)) - - if err != nil { - return UniversalAuthLoginResponse{}, NewGenericRequestError(operationCallUniversalAuthLogin, err) - } - - if response.IsError() { - return UniversalAuthLoginResponse{}, NewAPIErrorWithResponse(operationCallUniversalAuthLogin, response, nil) - } - - return universalAuthLoginResponse, nil -} - -func CallMachineIdentityRefreshAccessToken(httpClient *resty.Client, request UniversalAuthRefreshRequest) (UniversalAuthRefreshResponse, error) { - var universalAuthRefreshResponse UniversalAuthRefreshResponse - response, err := httpClient. - R(). - SetResult(&universalAuthRefreshResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v1/auth/token/renew", config.INFISICAL_URL)) - - if err != nil { - return UniversalAuthRefreshResponse{}, NewGenericRequestError(operationCallMachineIdentityRefreshAccessToken, err) - } - - if response.IsError() { - return UniversalAuthRefreshResponse{}, NewAPIErrorWithResponse(operationCallMachineIdentityRefreshAccessToken, response, nil) - } - - return universalAuthRefreshResponse, nil -} - -func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Request) (GetRawSecretsV3Response, error) { - var getRawSecretsV3Response GetRawSecretsV3Response - req := httpClient. - R(). - SetResult(&getRawSecretsV3Response). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - SetQueryParam("workspaceId", request.WorkspaceId). - SetQueryParam("environment", request.Environment). - SetQueryParam("secretPath", request.SecretPath) - - if request.TagSlugs != "" { - req.SetQueryParam("tagSlugs", request.TagSlugs) - } - - if request.IncludeImport { - req.SetQueryParam("include_imports", "true") - } - if request.Recursive { - req.SetQueryParam("recursive", "true") - } - - if request.ExpandSecretReferences { - req.SetQueryParam("expandSecretReferences", "true") - } - - response, err := req.Get(fmt.Sprintf("%v/v3/secrets/raw", config.INFISICAL_URL)) - - if err != nil { - return GetRawSecretsV3Response{}, NewGenericRequestError(operationCallGetRawSecretsV3, err) - } - - if response.IsError() && - (strings.Contains(response.String(), "bot_not_found_error") || - strings.Contains(strings.ToLower(response.String()), "failed to find bot key") || - strings.Contains(strings.ToLower(response.String()), "bot is not active")) { - additionalContext := fmt.Sprintf(`Project with id %s is incompatible with your current CLI version. Upgrade your project by visiting the project settings page. If you're self-hosting and project upgrade option isn't yet available, contact your administrator to upgrade your Infisical instance to the latest release.`, request.WorkspaceId) - return GetRawSecretsV3Response{}, NewAPIErrorWithResponse(operationCallGetRawSecretsV3, response, &additionalContext) - } - - if response.IsError() { - return GetRawSecretsV3Response{}, NewAPIErrorWithResponse(operationCallGetRawSecretsV3, response, nil) - } - - getRawSecretsV3Response.ETag = response.Header().Get(("etag")) - - return getRawSecretsV3Response, nil -} - -func CallFetchSingleSecretByName(httpClient *resty.Client, request GetRawSecretV3ByNameRequest) (GetRawSecretV3ByNameResponse, error) { - var getRawSecretV3ByNameResponse GetRawSecretV3ByNameResponse - response, err := httpClient. - R(). - SetHeader("User-Agent", USER_AGENT). - SetResult(&getRawSecretV3ByNameResponse). - SetBody(request). - SetQueryParam("expandSecretReferences", "true"). - SetQueryParam("include_imports", "true"). - SetQueryParam("environment", request.Environment). - SetQueryParam("secretPath", request.SecretPath). - SetQueryParam("workspaceId", request.WorkspaceID). - SetQueryParam("type", "shared"). - Get(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) - - if err != nil { - return GetRawSecretV3ByNameResponse{}, NewGenericRequestError(operationCallFetchSingleSecretByName, err) - } - - if response.IsError() { - return GetRawSecretV3ByNameResponse{}, NewAPIErrorWithResponse(operationCallFetchSingleSecretByName, response, nil) - } - - getRawSecretV3ByNameResponse.ETag = response.Header().Get(("etag")) - - return getRawSecretV3ByNameResponse, nil -} - -func CallCreateDynamicSecretLeaseV1(httpClient *resty.Client, request CreateDynamicSecretLeaseV1Request) (CreateDynamicSecretLeaseV1Response, error) { - var createDynamicSecretLeaseResponse CreateDynamicSecretLeaseV1Response - response, err := httpClient. - R(). - SetResult(&createDynamicSecretLeaseResponse). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v1/dynamic-secrets/leases", config.INFISICAL_URL)) - - if err != nil { - return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unable to complete api request [err=%w]", err) - } - - if response.IsError() { - return CreateDynamicSecretLeaseV1Response{}, fmt.Errorf("CreateDynamicSecretLeaseV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) - } - - return createDynamicSecretLeaseResponse, nil -} - -func CallCreateRawSecretsV3(httpClient *resty.Client, request CreateRawSecretV3Request) error { - response, err := httpClient. - R(). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) - - if err != nil { - return NewGenericRequestError(operationCallCreateRawSecretsV3, err) - } - - if response.IsError() { - return NewAPIErrorWithResponse(operationCallCreateRawSecretsV3, response, nil) - } - - return nil -} - -func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByNameV3Request) error { - response, err := httpClient. - R(). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Patch(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) - - if err != nil { - return NewGenericRequestError(operationCallUpdateRawSecretsV3, err) - } - - if response.IsError() { - return NewAPIErrorWithResponse(operationCallUpdateRawSecretsV3, response, nil) - } - - return nil -} - -func CallRegisterGatewayIdentityV1(httpClient *resty.Client) (*GetRelayCredentialsResponseV1, error) { - var resBody GetRelayCredentialsResponseV1 - response, err := httpClient. - R(). - SetResult(&resBody). - SetHeader("User-Agent", USER_AGENT). - Post(fmt.Sprintf("%v/v1/gateways/register-identity", config.INFISICAL_URL)) - - if err != nil { - return nil, NewGenericRequestError(operationCallRegisterGatewayIdentityV1, err) - } - - if response.IsError() { - return nil, NewAPIErrorWithResponse(operationCallRegisterGatewayIdentityV1, response, nil) - } - - return &resBody, nil -} - -func CallExchangeRelayCertV1(httpClient *resty.Client, request ExchangeRelayCertRequestV1) (*ExchangeRelayCertResponseV1, error) { - var resBody ExchangeRelayCertResponseV1 - response, err := httpClient. - R(). - SetResult(&resBody). - SetBody(request). - SetHeader("User-Agent", USER_AGENT). - Post(fmt.Sprintf("%v/v1/gateways/exchange-cert", config.INFISICAL_URL)) - - if err != nil { - return nil, NewGenericRequestError(operationCallExchangeRelayCertV1, err) - } - - if response.IsError() { - return nil, NewAPIErrorWithResponse(operationCallExchangeRelayCertV1, response, nil) - } - - return &resBody, nil -} - -func CallGatewayHeartBeatV1(httpClient *resty.Client) error { - response, err := httpClient. - R(). - SetHeader("User-Agent", USER_AGENT). - Post(fmt.Sprintf("%v/v1/gateways/heartbeat", config.INFISICAL_URL)) - - if err != nil { - return NewGenericRequestError(operationCallGatewayHeartBeatV1, err) - } - - if response.IsError() { - return NewAPIErrorWithResponse(operationCallGatewayHeartBeatV1, response, nil) - } - - return nil -} - -func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (BootstrapInstanceResponse, error) { - var resBody BootstrapInstanceResponse - response, err := httpClient. - R(). - SetResult(&resBody). - SetHeader("User-Agent", USER_AGENT). - SetBody(request). - Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain)) - - if err != nil { - return BootstrapInstanceResponse{}, NewGenericRequestError(operationCallBootstrapInstance, err) - } - - if response.IsError() { - return BootstrapInstanceResponse{}, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil) - } - - return resBody, nil -} diff --git a/cli/packages/api/errors.go b/cli/packages/api/errors.go deleted file mode 100644 index 4729d1264..000000000 --- a/cli/packages/api/errors.go +++ /dev/null @@ -1,80 +0,0 @@ -package api - -import ( - "fmt" - - "github.com/go-resty/resty/v2" - "github.com/infisical/go-sdk/packages/util" -) - -type GenericRequestError struct { - err error - operation string -} - -func (e *GenericRequestError) Error() string { - return fmt.Sprintf("%s: Unable to complete api request [err=%v]", e.operation, e.err) -} - -func NewGenericRequestError(operation string, err error) *GenericRequestError { - return &GenericRequestError{err: err, operation: operation} -} - -// APIError represents an error response from the API -type APIError struct { - AdditionalContext string `json:"additionalContext,omitempty"` - Operation string `json:"operation"` - Method string `json:"method"` - URL string `json:"url"` - StatusCode int `json:"statusCode"` - ErrorMessage string `json:"message,omitempty"` - ReqId string `json:"reqId,omitempty"` -} - -func (e *APIError) Error() string { - msg := fmt.Sprintf( - "%s Unsuccessful response [%v %v] [status-code=%v] [request-id=%v]", - e.Operation, - e.Method, - e.URL, - e.StatusCode, - e.ReqId, - ) - - if e.ErrorMessage != "" { - msg = fmt.Sprintf("%s [message=\"%s\"]", msg, e.ErrorMessage) - } - - if e.AdditionalContext != "" { - msg = fmt.Sprintf("%s [additional-context=\"%s\"]", msg, e.AdditionalContext) - } - - return msg -} - -func NewAPIErrorWithResponse(operation string, res *resty.Response, additionalContext *string) error { - errorMessage := util.TryParseErrorBody(res) - reqId := util.TryExtractReqId(res) - - if res == nil { - return NewGenericRequestError(operation, fmt.Errorf("response is nil")) - } - - apiError := &APIError{ - Operation: operation, - Method: res.Request.Method, - URL: res.Request.URL, - StatusCode: res.StatusCode(), - ReqId: reqId, - } - - if additionalContext != nil && *additionalContext != "" { - apiError.AdditionalContext = *additionalContext - } - - if errorMessage != "" { - apiError.ErrorMessage = errorMessage - } - - return apiError -} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go deleted file mode 100644 index 9bf666e44..000000000 --- a/cli/packages/api/model.go +++ /dev/null @@ -1,689 +0,0 @@ -package api - -import "time" - -// Stores info for login one -type LoginOneRequest struct { - Email string `json:"email"` - ClientPublicKey string `json:"clientPublicKey"` -} - -type LoginOneResponse struct { - ServerPublicKey string `json:"serverPublicKey"` - ServerSalt string `json:"salt"` -} - -// Stores info for login two - -type LoginTwoRequest struct { - Email string `json:"email"` - ClientProof string `json:"clientProof"` -} - -type LoginTwoResponse struct { - JTWToken string `json:"token"` - RefreshToken string `json:"refreshToken"` - PublicKey string `json:"publicKey"` - EncryptedPrivateKey string `json:"encryptedPrivateKey"` - IV string `json:"iv"` - Tag string `json:"tag"` -} - -type PullSecretsRequest struct { - Environment string `json:"environment"` -} - -type PullSecretsResponse struct { - Secrets []struct { - ID string `json:"_id"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretKeyHash string `json:"secretKeyHash"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretValueHash string `json:"secretValueHash"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - User string `json:"user,omitempty"` - } `json:"secrets"` - Key struct { - ID string `json:"_id"` - EncryptedKey string `json:"encryptedKey"` - Nonce string `json:"nonce"` - Sender struct { - ID string `json:"_id"` - Email string `json:"email"` - CustomerID string `json:"customerId"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - V int `json:"__v"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - PublicKey string `json:"publicKey"` - } `json:"sender"` - Receiver string `json:"receiver"` - Workspace string `json:"workspace"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - } `json:"key"` -} - -type PullSecretsByInfisicalTokenResponse struct { - Secrets []struct { - ID string `json:"_id"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKey struct { - Workspace string `json:"workspace"` - Ciphertext string `json:"ciphertext"` - Iv string `json:"iv"` - Tag string `json:"tag"` - Hash string `json:"hash"` - } `json:"secretKey"` - SecretValue struct { - Workspace string `json:"workspace"` - Ciphertext string `json:"ciphertext"` - Iv string `json:"iv"` - Tag string `json:"tag"` - Hash string `json:"hash"` - } `json:"secretValue"` - } `json:"secrets"` - Key struct { - EncryptedKey string `json:"encryptedKey"` - Nonce string `json:"nonce"` - Sender struct { - PublicKey string `json:"publicKey"` - } `json:"sender"` - Receiver struct { - RefreshVersion int `json:"refreshVersion"` - ID string `json:"_id"` - Email string `json:"email"` - CustomerID string `json:"customerId"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - V int `json:"__v"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - PublicKey string `json:"publicKey"` - } `json:"receiver"` - Workspace string `json:"workspace"` - } `json:"key"` -} - -type GetWorkSpacesResponse struct { - Workspaces []struct { - ID string `json:"_id"` - Name string `json:"name"` - Plan string `json:"plan,omitempty"` - V int `json:"__v"` - OrganizationId string `json:"orgId"` - } `json:"workspaces"` -} - -type GetProjectByIdResponse struct { - Project Project `json:"workspace"` -} - -type GetOrganizationsResponse struct { - Organizations []struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"organizations"` -} - -type SelectOrganizationResponse struct { - Token string `json:"token"` - MfaEnabled bool `json:"isMfaEnabled"` - MfaMethod string `json:"mfaMethod"` -} - -type SelectOrganizationRequest struct { - OrganizationId string `json:"organizationId"` -} - -type Secret struct { - SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"` - SecretKeyIV string `json:"secretKeyIV,omitempty"` - SecretKeyTag string `json:"secretKeyTag,omitempty"` - SecretKeyHash string `json:"secretKeyHash,omitempty"` - SecretValueCiphertext string `json:"secretValueCiphertext,omitempty"` - SecretValueIV string `json:"secretValueIV,omitempty"` - SecretValueTag string `json:"secretValueTag,omitempty"` - SecretValueHash string `json:"secretValueHash,omitempty"` - SecretCommentCiphertext string `json:"secretCommentCiphertext,omitempty"` - SecretCommentIV string `json:"secretCommentIV,omitempty"` - SecretCommentTag string `json:"secretCommentTag,omitempty"` - SecretCommentHash string `json:"secretCommentHash,omitempty"` - Type string `json:"type,omitempty"` - ID string `json:"id,omitempty"` - PlainTextKey string `json:"plainTextKey"` -} - -type Project struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` -} - -type RawSecret struct { - SecretKey string `json:"secretKey,omitempty"` - SecretValue string `json:"secretValue,omitempty"` - Type string `json:"type,omitempty"` - SecretComment string `json:"secretComment,omitempty"` - ID string `json:"id,omitempty"` -} - -type GetEncryptedWorkspaceKeyRequest struct { - WorkspaceId string `json:"workspaceId"` -} - -type GetEncryptedWorkspaceKeyResponse struct { - ID string `json:"_id"` - EncryptedKey string `json:"encryptedKey"` - Nonce string `json:"nonce"` - Sender struct { - ID string `json:"_id"` - Email string `json:"email"` - RefreshVersion int `json:"refreshVersion"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - V int `json:"__v"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - PublicKey string `json:"publicKey"` - } `json:"sender"` - Receiver string `json:"receiver"` - Workspace string `json:"workspace"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` -} - -type GetSecretsByWorkspaceIdAndEnvironmentRequest struct { - EnvironmentName string `json:"environmentName"` - WorkspaceId string `json:"workspaceId"` -} - -type GetServiceTokenDetailsResponse struct { - ID string `json:"_id"` - Name string `json:"name"` - Workspace string `json:"workspace"` - ExpiresAt time.Time `json:"expiresAt"` - EncryptedKey string `json:"encryptedKey"` - Iv string `json:"iv"` - Tag string `json:"tag"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - Scopes []struct { - Environment string `json:"environment"` - SecretPath string `json:"secretPath"` - } `json:"scopes"` -} - -type GetAccessibleEnvironmentsRequest struct { - WorkspaceId string `json:"workspaceId"` -} - -type GetAccessibleEnvironmentsResponse struct { - AccessibleEnvironments []struct { - Name string `json:"name"` - Slug string `json:"slug"` - IsWriteDenied bool `json:"isWriteDenied"` - } `json:"accessibleEnvironments"` -} - -type GetLoginOneV2Request struct { - Email string `json:"email"` - ClientPublicKey string `json:"clientPublicKey"` -} - -type GetLoginOneV2Response struct { - ServerPublicKey string `json:"serverPublicKey"` - Salt string `json:"salt"` -} - -type GetLoginTwoV2Request struct { - Email string `json:"email"` - ClientProof string `json:"clientProof"` - Password string `json:"password"` -} - -type GetLoginTwoV2Response struct { - MfaEnabled bool `json:"mfaEnabled"` - EncryptionVersion int `json:"encryptionVersion"` - Token string `json:"token"` - PublicKey string `json:"publicKey"` - EncryptedPrivateKey string `json:"encryptedPrivateKey"` - Iv string `json:"iv"` - Tag string `json:"tag"` - ProtectedKey string `json:"protectedKey"` - ProtectedKeyIV string `json:"protectedKeyIV"` - ProtectedKeyTag string `json:"protectedKeyTag"` - RefreshToken string `json:"RefreshToken"` -} - -type VerifyMfaTokenRequest struct { - Email string `json:"email"` - MFAToken string `json:"mfaToken"` - MFAMethod string `json:"mfaMethod"` -} - -type VerifyMfaTokenResponse struct { - EncryptionVersion int `json:"encryptionVersion"` - Token string `json:"token"` - PublicKey string `json:"publicKey"` - EncryptedPrivateKey string `json:"encryptedPrivateKey"` - Iv string `json:"iv"` - Tag string `json:"tag"` - ProtectedKey string `json:"protectedKey"` - ProtectedKeyIV string `json:"protectedKeyIV"` - ProtectedKeyTag string `json:"protectedKeyTag"` - RefreshToken string `json:"refreshToken"` -} - -type VerifyMfaTokenErrorResponse struct { - Type string `json:"type"` - Message string `json:"message"` - Context struct { - Code string `json:"code"` - TriesLeft int `json:"triesLeft"` - } `json:"context"` - Level int `json:"level"` - LevelName string `json:"level_name"` - StatusCode int `json:"status_code"` - DatetimeIso time.Time `json:"datetime_iso"` - Application string `json:"application"` - Extra []interface{} `json:"extra"` -} - -type GetNewAccessTokenWithRefreshTokenResponse struct { - Token string `json:"token"` -} - -type GetEncryptedSecretsV3Request struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - SecretPath string `json:"secretPath"` - IncludeImport bool `json:"include_imports"` - Recursive bool `json:"recursive"` -} - -type GetFoldersV1Request struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - FoldersPath string `json:"foldersPath"` -} - -type GetFoldersV1Response struct { - Folders []struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"folders"` -} - -type CreateFolderV1Request struct { - FolderName string `json:"name"` - WorkspaceId string `json:"workspaceId"` - Environment string `json:"environment"` - Path string `json:"path"` -} - -type CreateFolderV1Response struct { - Folder struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"folder"` -} - -type DeleteFolderV1Request struct { - FolderName string `json:"folderName"` - WorkspaceId string `json:"workspaceId"` - Environment string `json:"environment"` - Directory string `json:"directory"` -} - -type DeleteFolderV1Response struct { - Folders []struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"folders"` -} - -type EncryptedSecretV3 struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Tags []struct { - ID string `json:"_id"` - Name string `json:"name"` - Slug string `json:"slug"` - Workspace string `json:"workspace"` - } `json:"tags"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - Algorithm string `json:"algorithm"` - KeyEncoding string `json:"keyEncoding"` - Folder string `json:"folder"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` -} - -type ImportedSecretV3 struct { - Environment string `json:"environment"` - FolderId string `json:"folderId"` - SecretPath string `json:"secretPath"` - Secrets []EncryptedSecretV3 `json:"secrets"` -} - -type ImportedRawSecretV3 struct { - SecretPath string `json:"secretPath"` - Environment string `json:"environment"` - FolderId string `json:"folderId"` - Secrets []struct { - ID string `json:"id"` - Workspace string `json:"workspace"` - Environment string `json:"environment"` - Version int `json:"version"` - Type string `json:"type"` - SecretKey string `json:"secretKey"` - SecretValue string `json:"secretValue"` - SecretComment string `json:"secretComment"` - } `json:"secrets"` -} - -type GetEncryptedSecretsV3Response struct { - Secrets []EncryptedSecretV3 `json:"secrets"` - ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"` -} - -type CreateSecretV3Request struct { - SecretName string `json:"secretName"` - WorkspaceID string `json:"workspaceId"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - SecretPath string `json:"secretPath"` -} - -type CreateRawSecretV3Request struct { - SecretName string `json:"-"` - WorkspaceID string `json:"workspaceId"` - Type string `json:"type,omitempty"` - Environment string `json:"environment"` - SecretPath string `json:"secretPath,omitempty"` - SecretValue string `json:"secretValue"` - SecretComment string `json:"secretComment,omitempty"` - SkipMultilineEncoding bool `json:"skipMultilineEncoding,omitempty"` -} - -type DeleteSecretV3Request struct { - SecretName string `json:"secretName"` - WorkspaceId string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type,omitempty"` - SecretPath string `json:"secretPath,omitempty"` -} - -type UpdateSecretByNameV3Request struct { - WorkspaceID string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` -} - -type UpdateRawSecretByNameV3Request struct { - SecretName string `json:"-"` - WorkspaceID string `json:"workspaceId"` - Environment string `json:"environment"` - SecretPath string `json:"secretPath,omitempty"` - SecretValue string `json:"secretValue"` - Type string `json:"type,omitempty"` -} - -type GetSingleSecretByNameV3Request struct { - SecretName string `json:"secretName"` - WorkspaceId string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` -} - -type GetSingleSecretByNameSecretResponse struct { - Secrets []struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - Algorithm string `json:"algorithm"` - KeyEncoding string `json:"keyEncoding"` - Folder string `json:"folder"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - } `json:"secrets"` -} - -type ScopePermission struct { - Environment string `json:"environment"` - SecretPath string `json:"secretPath"` -} - -type CreateServiceTokenRequest struct { - Name string `json:"name"` - WorkspaceId string `json:"workspaceId"` - Scopes []ScopePermission `json:"scopes"` - ExpiresIn int `json:"expiresIn"` - EncryptedKey string `json:"encryptedKey"` - Iv string `json:"iv"` - Tag string `json:"tag"` - RandomBytes string `json:"randomBytes"` - Permissions []string `json:"permissions"` -} - -type ServiceTokenData struct { - ID string `json:"_id"` - Name string `json:"name"` - Workspace string `json:"workspace"` - Scopes []interface{} `json:"scopes"` - User string `json:"user"` - LastUsed time.Time `json:"lastUsed"` - Permissions []string `json:"permissions"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` -} - -type CreateServiceTokenResponse struct { - ServiceToken string `json:"serviceToken"` - ServiceTokenData ServiceTokenData `json:"serviceTokenData"` -} - -type UniversalAuthLoginRequest struct { - ClientSecret string `json:"clientSecret"` - ClientId string `json:"clientId"` -} - -type UniversalAuthLoginResponse struct { - AccessToken string `json:"accessToken"` - AccessTokenTTL int `json:"expiresIn"` - TokenType string `json:"tokenType"` - AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` -} - -type UniversalAuthRefreshRequest struct { - AccessToken string `json:"accessToken"` -} - -type UniversalAuthRefreshResponse struct { - AccessToken string `json:"accessToken"` - AccessTokenTTL int `json:"expiresIn"` - TokenType string `json:"tokenType"` - AccessTokenMaxTTL int `json:"accessTokenMaxTTL"` -} - -type CreateDynamicSecretLeaseV1Request struct { - Environment string `json:"environment"` - ProjectSlug string `json:"projectSlug"` - SecretPath string `json:"secretPath,omitempty"` - Slug string `json:"slug"` - TTL string `json:"ttl,omitempty"` -} - -type CreateDynamicSecretLeaseV1Response struct { - Lease struct { - Id string `json:"id"` - ExpireAt time.Time `json:"expireAt"` - } `json:"lease"` - DynamicSecret struct { - Id string `json:"id"` - DefaultTTL string `json:"defaultTTL"` - MaxTTL string `json:"maxTTL"` - Type string `json:"type"` - } `json:"dynamicSecret"` - Data map[string]interface{} `json:"data"` -} - -type GetRawSecretsV3Request struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - SecretPath string `json:"secretPath"` - IncludeImport bool `json:"include_imports"` - Recursive bool `json:"recursive"` - TagSlugs string `json:"tagSlugs,omitempty"` - ExpandSecretReferences bool `json:"expandSecretReferences,omitempty"` -} - -type GetRawSecretsV3Response struct { - Secrets []struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKey string `json:"secretKey"` - SecretValue string `json:"secretValue"` - SecretComment string `json:"secretComment"` - SecretPath string `json:"secretPath"` - } `json:"secrets"` - Imports []ImportedRawSecretV3 `json:"imports"` - ETag string -} - -type GetRawSecretV3ByNameRequest struct { - SecretName string `json:"secretName"` - WorkspaceID string `json:"workspaceId"` - Type string `json:"type,omitempty"` - Environment string `json:"environment"` - SecretPath string `json:"secretPath,omitempty"` -} - -type GetRawSecretV3ByNameResponse struct { - Secret struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKey string `json:"secretKey"` - SecretValue string `json:"secretValue"` - SecretComment string `json:"secretComment"` - SecretPath string `json:"secretPath"` - } `json:"secret"` - ETag string -} - -type GetRelayCredentialsResponseV1 struct { - TurnServerUsername string `json:"turnServerUsername"` - TurnServerPassword string `json:"turnServerPassword"` - TurnServerRealm string `json:"turnServerRealm"` - TurnServerAddress string `json:"turnServerAddress"` - InfisicalStaticIp string `json:"infisicalStaticIp"` -} - -type ExchangeRelayCertRequestV1 struct { - RelayAddress string `json:"relayAddress"` -} - -type ExchangeRelayCertResponseV1 struct { - SerialNumber string `json:"serialNumber"` - PrivateKey string `json:"privateKey"` - Certificate string `json:"certificate"` - CertificateChain string `json:"certificateChain"` -} - -type BootstrapInstanceRequest struct { - Email string `json:"email"` - Password string `json:"password"` - Organization string `json:"organization"` - Domain string `json:"domain"` -} - -type BootstrapInstanceResponse struct { - Message string `json:"message"` - Identity BootstrapIdentity `json:"identity"` - Organization BootstrapOrganization `json:"organization"` - User BootstrapUser `json:"user"` -} - -type BootstrapIdentity struct { - ID string `json:"id"` - Name string `json:"name"` - Credentials BootstrapIdentityCredentials `json:"credentials"` -} - -type BootstrapIdentityCredentials struct { - Token string `json:"token"` -} - -type BootstrapOrganization struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` -} - -type BootstrapUser struct { - ID string `json:"id"` - Email string `json:"email"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Username string `json:"username"` - SuperAdmin bool `json:"superAdmin"` -} diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go deleted file mode 100644 index cb10050dd..000000000 --- a/cli/packages/cmd/agent.go +++ /dev/null @@ -1,1081 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "os/exec" - "os/signal" - "path" - "runtime" - "slices" - "sync" - "syscall" - "text/template" - "time" - - infisicalSdk "github.com/infisical/go-sdk" - "github.com/rs/zerolog/log" - "gopkg.in/yaml.v2" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/spf13/cobra" -) - -const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com" - -// duration to reduce from expiry of dynamic leases so that it gets triggered before expiry -const DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER = -15 - -type Config struct { - Infisical InfisicalConfig `yaml:"infisical"` - Auth AuthConfig `yaml:"auth"` - Sinks []Sink `yaml:"sinks"` - Templates []Template `yaml:"templates"` -} - -type InfisicalConfig struct { - Address string `yaml:"address"` - ExitAfterAuth bool `yaml:"exit-after-auth"` -} - -type AuthConfig struct { - Type string `yaml:"type"` - Config interface{} `yaml:"config"` -} - -type UniversalAuth struct { - ClientIDPath string `yaml:"client-id"` - ClientSecretPath string `yaml:"client-secret"` - RemoveClientSecretOnRead bool `yaml:"remove_client_secret_on_read"` -} - -type KubernetesAuth struct { - IdentityID string `yaml:"identity-id"` - ServiceAccountToken string `yaml:"service-account-token"` -} - -type AzureAuth struct { - IdentityID string `yaml:"identity-id"` -} - -type GcpIdTokenAuth struct { - IdentityID string `yaml:"identity-id"` -} - -type GcpIamAuth struct { - IdentityID string `yaml:"identity-id"` - ServiceAccountKey string `yaml:"service-account-key"` -} - -type AwsIamAuth struct { - IdentityID string `yaml:"identity-id"` -} - -type Sink struct { - Type string `yaml:"type"` - Config SinkDetails `yaml:"config"` -} - -type SinkDetails struct { - Path string `yaml:"path"` -} - -type Template struct { - SourcePath string `yaml:"source-path"` - Base64TemplateContent string `yaml:"base64-template-content"` - DestinationPath string `yaml:"destination-path"` - TemplateContent string `yaml:"template-content"` - - Config struct { // Configurations for the template - PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret - Execute struct { - Command string `yaml:"command"` // Command to execute once the template has been rendered - Timeout int64 `yaml:"timeout"` // Timeout for the command - } `yaml:"execute"` // Command to execute once the template has been rendered - } `yaml:"config"` -} - -type DynamicSecretLease struct { - LeaseID string - ExpireAt time.Time - Environment string - SecretPath string - Slug string - ProjectSlug string - Data map[string]interface{} - TemplateIDs []int -} - -type DynamicSecretLeaseManager struct { - leases []DynamicSecretLease - mutex sync.Mutex -} - -func (d *DynamicSecretLeaseManager) Prune() { - d.mutex.Lock() - defer d.mutex.Unlock() - - d.leases = slices.DeleteFunc(d.leases, func(s DynamicSecretLease) bool { - return time.Now().After(s.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second)) - }) -} - -func (d *DynamicSecretLeaseManager) Append(lease DynamicSecretLease) { - d.mutex.Lock() - defer d.mutex.Unlock() - - index := slices.IndexFunc(d.leases, func(s DynamicSecretLease) bool { - if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug { - return true - } - return false - }) - - if index != -1 { - d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, lease.TemplateIDs...) - return - } - d.leases = append(d.leases, lease) -} - -func (d *DynamicSecretLeaseManager) RegisterTemplate(projectSlug, environment, secretPath, slug string, templateId int) { - d.mutex.Lock() - defer d.mutex.Unlock() - - index := slices.IndexFunc(d.leases, func(lease DynamicSecretLease) bool { - if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug { - return true - } - return false - }) - - if index != -1 { - d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, templateId) - } -} - -func (d *DynamicSecretLeaseManager) GetLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease { - d.mutex.Lock() - defer d.mutex.Unlock() - - for _, lease := range d.leases { - if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug { - return &lease - } - } - - return nil -} - -// for a given template find the first expiring lease -// The bool indicates whether it contains valid expiry list -func (d *DynamicSecretLeaseManager) GetFirstExpiringLeaseTime(templateId int) (time.Time, bool) { - d.mutex.Lock() - defer d.mutex.Unlock() - - if len(d.leases) == 0 { - return time.Time{}, false - } - - var firstExpiry time.Time - for i, el := range d.leases { - if i == 0 { - firstExpiry = el.ExpireAt - } - newLeaseTime := el.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second) - if newLeaseTime.Before(firstExpiry) { - firstExpiry = newLeaseTime - } - } - return firstExpiry, true -} - -func NewDynamicSecretLeaseManager(sigChan chan os.Signal) *DynamicSecretLeaseManager { - manager := &DynamicSecretLeaseManager{} - return manager -} - -func ReadFile(filePath string) ([]byte, error) { - return ioutil.ReadFile(filePath) -} - -func ExecuteCommandWithTimeout(command string, timeout int64) error { - - shell := [2]string{"sh", "-c"} - if runtime.GOOS == "windows" { - shell = [2]string{"cmd", "/C"} - } else { - currentShell := os.Getenv("SHELL") - if currentShell != "" { - shell[0] = currentShell - } - } - - ctx := context.Background() - if timeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - } - - cmd := exec.CommandContext(ctx, shell[0], shell[1], command) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { // type assertion - if exitError.ProcessState.ExitCode() == -1 { - return fmt.Errorf("command timed out") - } - } - return err - } else { - return nil - } -} - -func FileExists(filepath string) bool { - info, err := os.Stat(filepath) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} - -// WriteToFile writes data to the specified file path. -func WriteBytesToFile(data *bytes.Buffer, outputPath string) error { - outputFile, err := os.Create(outputPath) - if err != nil { - return err - } - defer outputFile.Close() - - _, err = outputFile.Write(data.Bytes()) - return err -} - -func ParseAuthConfig(authConfigFile []byte, destination interface{}) error { - if err := yaml.Unmarshal(authConfigFile, destination); err != nil { - return err - } - - return nil -} - -func ParseAgentConfig(configFile []byte) (*Config, error) { - var rawConfig struct { - Infisical InfisicalConfig `yaml:"infisical"` - Auth struct { - Type string `yaml:"type"` - Config map[string]interface{} `yaml:"config"` - } `yaml:"auth"` - Sinks []Sink `yaml:"sinks"` - Templates []Template `yaml:"templates"` - } - - if err := yaml.Unmarshal(configFile, &rawConfig); err != nil { - return nil, err - } - - // Set defaults - if rawConfig.Infisical.Address == "" { - rawConfig.Infisical.Address = DEFAULT_INFISICAL_CLOUD_URL - } - - config.INFISICAL_URL = util.AppendAPIEndpoint(rawConfig.Infisical.Address) - - log.Info().Msgf("Infisical instance address set to %s", rawConfig.Infisical.Address) - - config := &Config{ - Infisical: rawConfig.Infisical, - Auth: AuthConfig{ - Type: rawConfig.Auth.Type, - Config: rawConfig.Auth.Config, - }, - Sinks: rawConfig.Sinks, - Templates: rawConfig.Templates, - } - - return config, nil -} - -type secretArguments struct { - IsRecursive bool `json:"recursive"` - ShouldExpandSecretReferences *bool `json:"expandSecretReferences,omitempty"` -} - -func (s *secretArguments) SetDefaults() { - if s.ShouldExpandSecretReferences == nil { - var bool = true - s.ShouldExpandSecretReferences = &bool - } -} - -func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string, ...string) ([]models.SingleEnvironmentVariable, error) { - // ...string is because golang doesn't have optional arguments. - // thus we make it slice and pick it only first element - return func(projectID, envSlug, secretPath string, args ...string) ([]models.SingleEnvironmentVariable, error) { - var parsedArguments secretArguments - // to make it optional - if len(args) > 0 { - err := json.Unmarshal([]byte(args[0]), &parsedArguments) - if err != nil { - return nil, err - } - } - - parsedArguments.SetDefaults() - - res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, true, parsedArguments.IsRecursive, "", *parsedArguments.ShouldExpandSecretReferences) - if err != nil { - return nil, err - } - - if existingEtag != res.Etag { - *currentEtag = res.Etag - } - - return res.Secrets, nil - } -} - -func getSingleSecretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string, string) (models.SingleEnvironmentVariable, error) { - return func(projectID, envSlug, secretPath, secretName string) (models.SingleEnvironmentVariable, error) { - secret, requestEtag, err := util.GetSinglePlainTextSecretByNameV3(accessToken, projectID, envSlug, secretPath, secretName) - if err != nil { - return models.SingleEnvironmentVariable{}, err - } - - if existingEtag != requestEtag { - *currentEtag = requestEtag - } - - return secret, nil - } -} - -func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *DynamicSecretLeaseManager, templateId int) func(...string) (map[string]interface{}, error) { - return func(args ...string) (map[string]interface{}, error) { - argLength := len(args) - if argLength != 4 && argLength != 5 { - return nil, fmt.Errorf("invalid arguments found for dynamic-secret function. Check template %d", templateId) - } - - projectSlug, envSlug, secretPath, slug, ttl := args[0], args[1], args[2], args[3], "" - if argLength == 5 { - ttl = args[4] - } - dynamicSecretData := dynamicSecretManager.GetLease(projectSlug, envSlug, secretPath, slug) - if dynamicSecretData != nil { - dynamicSecretManager.RegisterTemplate(projectSlug, envSlug, secretPath, slug, templateId) - return dynamicSecretData.Data, nil - } - - res, err := util.CreateDynamicSecretLease(accessToken, projectSlug, envSlug, secretPath, slug, ttl) - if err != nil { - return nil, err - } - - dynamicSecretManager.Append(DynamicSecretLease{LeaseID: res.Lease.Id, ExpireAt: res.Lease.ExpireAt, Environment: envSlug, SecretPath: secretPath, Slug: slug, ProjectSlug: projectSlug, Data: res.Data, TemplateIDs: []int{templateId}}) - return res.Data, nil - } -} - -func ProcessTemplate(templateId int, templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretManager *DynamicSecretLeaseManager) (*bytes.Buffer, error) { - // custom template function to fetch secrets from Infisical - secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) - dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, templateId) - getSingleSecretFunction := getSingleSecretTemplateFunction(accessToken, existingEtag, currentEtag) - funcs := template.FuncMap{ - "secret": secretFunction, // depreciated - "listSecrets": secretFunction, - "dynamic_secret": dynamicSecretFunction, - "getSecretByName": getSingleSecretFunction, - "minus": func(a, b int) int { - return a - b - }, - "add": func(a, b int) int { - return a + b - }, - } - - templateName := path.Base(templatePath) - tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath) - if err != nil { - return nil, err - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return nil, err - } - - return &buf, nil -} - -func ProcessBase64Template(templateId int, encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) { - // custom template function to fetch secrets from Infisical - decoded, err := base64.StdEncoding.DecodeString(encodedTemplate) - if err != nil { - return nil, err - } - - templateString := string(decoded) - - secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this - dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId) - funcs := template.FuncMap{ - "secret": secretFunction, - "dynamic_secret": dynamicSecretFunction, - } - - templateName := "base64Template" - - tmpl, err := template.New(templateName).Funcs(funcs).Parse(templateString) - if err != nil { - return nil, err - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return nil, err - } - - return &buf, nil -} - -func ProcessLiteralTemplate(templateId int, templateString string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) { - secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this - dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId) - funcs := template.FuncMap{ - "secret": secretFunction, - "dynamic_secret": dynamicSecretFunction, - } - - templateName := "literalTemplate" - - tmpl, err := template.New(templateName).Funcs(funcs).Parse(templateString) - if err != nil { - return nil, err - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return nil, err - } - - return &buf, nil -} - -type AgentManager struct { - accessToken string - accessTokenTTL time.Duration - accessTokenMaxTTL time.Duration - accessTokenFetchedTime time.Time - accessTokenRefreshedTime time.Time - mutex sync.Mutex - filePaths []Sink // Store file paths if needed - templates []Template - dynamicSecretLeases *DynamicSecretLeaseManager - - authConfigBytes []byte - authStrategy util.AuthStrategyType - - newAccessTokenNotificationChan chan bool - removeUniversalAuthClientSecretOnRead bool - cachedUniversalAuthClientSecret string - exitAfterAuth bool - - infisicalClient infisicalSdk.InfisicalClientInterface -} - -type NewAgentMangerOptions struct { - FileDeposits []Sink - Templates []Template - - AuthConfigBytes []byte - AuthStrategy util.AuthStrategyType - - NewAccessTokenNotificationChan chan bool - ExitAfterAuth bool -} - -func NewAgentManager(options NewAgentMangerOptions) *AgentManager { - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - return &AgentManager{ - filePaths: options.FileDeposits, - templates: options.Templates, - - authConfigBytes: options.AuthConfigBytes, - authStrategy: options.AuthStrategy, - - newAccessTokenNotificationChan: options.NewAccessTokenNotificationChan, - exitAfterAuth: options.ExitAfterAuth, - - infisicalClient: infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, // ? Should we perhaps use a different user agent for the Agent for better analytics? - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }), - } - -} - -func (tm *AgentManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) { - tm.mutex.Lock() - defer tm.mutex.Unlock() - - tm.accessToken = token - tm.accessTokenTTL = accessTokenTTL - tm.accessTokenMaxTTL = accessTokenMaxTTL - - tm.newAccessTokenNotificationChan <- true -} - -func (tm *AgentManager) GetToken() string { - tm.mutex.Lock() - defer tm.mutex.Unlock() - - return tm.accessToken -} - -func (tm *AgentManager) FetchUniversalAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, e error) { - - var universalAuthConfig UniversalAuth - if err := ParseAuthConfig(tm.authConfigBytes, &universalAuthConfig); err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err) - } - - clientID, err := util.GetEnvVarOrFileContent(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME, universalAuthConfig.ClientIDPath) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get client id: %v", err) - } - - clientSecret, err := util.GetEnvVarOrFileContent("INFISICAL_UNIVERSAL_CLIENT_SECRET", universalAuthConfig.ClientSecretPath) - if err != nil { - if len(tm.cachedUniversalAuthClientSecret) == 0 { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get client secret: %v", err) - } - clientSecret = tm.cachedUniversalAuthClientSecret - } - - tm.cachedUniversalAuthClientSecret = clientSecret - if universalAuthConfig.RemoveClientSecretOnRead { - defer os.Remove(universalAuthConfig.ClientSecretPath) - } - - return tm.infisicalClient.Auth().UniversalAuthLogin(clientID, clientSecret) - -} - -func (tm *AgentManager) FetchKubernetesAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) { - - var kubernetesAuthConfig KubernetesAuth - if err := ParseAuthConfig(tm.authConfigBytes, &kubernetesAuthConfig); err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err) - } - - identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, kubernetesAuthConfig.IdentityID) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err) - } - - serviceAccountTokenPath := os.Getenv(util.INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME) - if serviceAccountTokenPath == "" { - serviceAccountTokenPath = kubernetesAuthConfig.ServiceAccountToken - if serviceAccountTokenPath == "" { - serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" - } - } - - return tm.infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath) - -} - -func (tm *AgentManager) FetchAzureAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) { - - var azureAuthConfig AzureAuth - if err := ParseAuthConfig(tm.authConfigBytes, &azureAuthConfig); err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err) - } - - identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, azureAuthConfig.IdentityID) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err) - } - - return tm.infisicalClient.Auth().AzureAuthLogin(identityId, "") - -} - -func (tm *AgentManager) FetchGcpIdTokenAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) { - - var gcpIdTokenAuthConfig GcpIdTokenAuth - if err := ParseAuthConfig(tm.authConfigBytes, &gcpIdTokenAuthConfig); err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err) - } - - identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, gcpIdTokenAuthConfig.IdentityID) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err) - } - - return tm.infisicalClient.Auth().GcpIdTokenAuthLogin(identityId) - -} - -func (tm *AgentManager) FetchGcpIamAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) { - - var gcpIamAuthConfig GcpIamAuth - if err := ParseAuthConfig(tm.authConfigBytes, &gcpIamAuthConfig); err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err) - } - - identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, gcpIamAuthConfig.IdentityID) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err) - } - - serviceAccountKeyPath := os.Getenv(util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME) - if serviceAccountKeyPath == "" { - // we don't need to read this file, because the service account key path is directly read inside the sdk - serviceAccountKeyPath = gcpIamAuthConfig.ServiceAccountKey - if serviceAccountKeyPath == "" { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("gcp service account key path not found") - } - } - - return tm.infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyPath) - -} - -func (tm *AgentManager) FetchAwsIamAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) { - - var awsIamAuthConfig AwsIamAuth - if err := ParseAuthConfig(tm.authConfigBytes, &awsIamAuthConfig); err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err) - } - - identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, awsIamAuthConfig.IdentityID) - - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err) - } - - return tm.infisicalClient.Auth().AwsIamAuthLogin(identityId) - -} - -// Fetches a new access token using client credentials -func (tm *AgentManager) FetchNewAccessToken() error { - - authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){ - util.AuthStrategy.UNIVERSAL_AUTH: tm.FetchUniversalAuthAccessToken, - util.AuthStrategy.KUBERNETES_AUTH: tm.FetchKubernetesAuthAccessToken, - util.AuthStrategy.AZURE_AUTH: tm.FetchAzureAuthAccessToken, - util.AuthStrategy.GCP_ID_TOKEN_AUTH: tm.FetchGcpIdTokenAuthAccessToken, - util.AuthStrategy.GCP_IAM_AUTH: tm.FetchGcpIamAuthAccessToken, - util.AuthStrategy.AWS_IAM_AUTH: tm.FetchAwsIamAuthAccessToken, - } - - if _, ok := authStrategies[tm.authStrategy]; !ok { - return fmt.Errorf("auth strategy %s not found", tm.authStrategy) - } - - credential, err := authStrategies[tm.authStrategy]() - - if err != nil { - return err - } - - accessTokenTTL := time.Duration(credential.ExpiresIn * int64(time.Second)) - accessTokenMaxTTL := time.Duration(credential.AccessTokenMaxTTL * int64(time.Second)) - - if accessTokenTTL <= time.Duration(5)*time.Second { - util.PrintErrorMessageAndExit("At this time, agent does not support refresh of tokens with 5 seconds or less ttl. Please increase access token ttl and try again") - } - - tm.accessTokenFetchedTime = time.Now() - tm.SetToken(credential.AccessToken, accessTokenTTL, accessTokenMaxTTL) - - return nil -} - -// Refreshes the existing access token -func (tm *AgentManager) RefreshAccessToken(accessToken string) error { - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - return err - } - - httpClient.SetRetryCount(10000). - SetRetryMaxWaitTime(20 * time.Second). - SetRetryWaitTime(5 * time.Second) - - response, err := api.CallMachineIdentityRefreshAccessToken(httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken}) - if err != nil { - return err - } - - accessTokenTTL := time.Duration(response.AccessTokenTTL * int(time.Second)) - accessTokenMaxTTL := time.Duration(response.AccessTokenMaxTTL * int(time.Second)) - tm.accessTokenRefreshedTime = time.Now() - - tm.SetToken(response.AccessToken, accessTokenTTL, accessTokenMaxTTL) - - return nil -} - -func (tm *AgentManager) ManageTokenLifecycle() { - for { - accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second)) - accessTokenRefreshedTime := tm.accessTokenRefreshedTime - - if accessTokenRefreshedTime.IsZero() { - accessTokenRefreshedTime = tm.accessTokenFetchedTime - } - - // Calculate next expiry time at 2/3 of the TTL - nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(tm.accessTokenTTL * 2 / 3) - - if tm.accessTokenFetchedTime.IsZero() && tm.accessTokenRefreshedTime.IsZero() { - // try to fetch token from sink files first - // if token is found, refresh the token right away and continue from there - isSavedTokenValid := false - token := tm.FetchTokenFromFiles() - if token != "" { - log.Info().Msg("found existing token in file, attempting to refresh...") - err := tm.RefreshAccessToken(token) - isSavedTokenValid = err == nil - if isSavedTokenValid { - log.Info().Msg("token refreshed successfully from saved file") - tm.accessTokenFetchedTime = time.Now() - } else { - log.Error().Msg("unable to refresh token from saved file") - } - } - - if !isSavedTokenValid { - // case: init login to get access token - log.Info().Msg("attempting to authenticate...") - err := tm.FetchNewAccessToken() - if err != nil { - log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err) - - // wait a bit before trying again - time.Sleep((30 * time.Second)) - continue - } - } - } else if time.Now().After(accessTokenMaxTTLExpiresInTime) { - // case: token has reached max ttl and we should re-authenticate entirely (cannot refresh) - log.Info().Msgf("token has reached max ttl, attempting to re authenticate...") - err := tm.FetchNewAccessToken() - if err != nil { - log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err) - - // wait a bit before trying again - time.Sleep((30 * time.Second)) - continue - } - } else { - // case: token ttl has expired, but the token is still within max ttl, so we can refresh - log.Info().Msgf("attempting to refresh existing token...") - err := tm.RefreshAccessToken(tm.GetToken()) - if err != nil { - log.Error().Msgf("unable to refresh token because %v. Will retry in 30 seconds", err) - - // wait a bit before trying again - time.Sleep((30 * time.Second)) - continue - } - } - - if tm.exitAfterAuth { - time.Sleep(25 * time.Second) - os.Exit(0) - } - - if accessTokenRefreshedTime.IsZero() { - accessTokenRefreshedTime = tm.accessTokenFetchedTime - } else { - accessTokenRefreshedTime = tm.accessTokenRefreshedTime - } - - // Recalculate next expiry time at 2/3 of the TTL - nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(tm.accessTokenTTL * 2 / 3) - accessTokenMaxTTLExpiresInTime = tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second)) - - if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) { - // case: Refreshed so close that the next refresh would occur beyond max ttl - // Sleep until we're at 2/3 of the remaining time to max TTL - remainingTime := accessTokenMaxTTLExpiresInTime.Sub(time.Now()) - time.Sleep(remainingTime * 2 / 3) - } else { - // Sleep until we're at 2/3 of the TTL - time.Sleep(tm.accessTokenTTL * 2 / 3) - } - } -} - -func (tm *AgentManager) WriteTokenToFiles() { - token := tm.GetToken() - for _, sinkFile := range tm.filePaths { - if sinkFile.Type == "file" { - err := ioutil.WriteFile(sinkFile.Config.Path, []byte(token), 0644) - if err != nil { - log.Error().Msgf("unable to write file sink to path '%s' because %v", sinkFile.Config.Path, err) - } - - log.Info().Msgf("new access token saved to file at path '%s'", sinkFile.Config.Path) - - } else { - log.Error().Msg("unsupported sink type. Only 'file' type is supported") - } - } -} - -func (tm *AgentManager) FetchTokenFromFiles() string { - for _, sinkFile := range tm.filePaths { - if sinkFile.Type == "file" { - tokenBytes, err := ioutil.ReadFile(sinkFile.Config.Path) - if err != nil { - log.Debug().Msgf("unable to read token from file '%s' because %v", sinkFile.Config.Path, err) - continue - } - - token := string(tokenBytes) - if token != "" { - return token - } - } - } - return "" -} - -func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) { - if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil { - log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err) - return - } - log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", template.SourcePath, template.DestinationPath) -} - -func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId int, sigChan chan os.Signal) { - - pollingInterval := time.Duration(5 * time.Minute) - - if secretTemplate.Config.PollingInterval != "" { - interval, err := util.ConvertPollingIntervalToTime(secretTemplate.Config.PollingInterval) - - if err != nil { - log.Error().Msgf("unable to convert polling interval to time because %v", err) - sigChan <- syscall.SIGINT - return - - } else { - pollingInterval = interval - } - } - - var existingEtag string - var currentEtag string - var firstRun = true - - execTimeout := secretTemplate.Config.Execute.Timeout - execCommand := secretTemplate.Config.Execute.Command - - for { - select { - case <-sigChan: - return - default: - { - tm.dynamicSecretLeases.Prune() - token := tm.GetToken() - if token != "" { - var processedTemplate *bytes.Buffer - var err error - - if secretTemplate.SourcePath != "" { - processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases) - } else if secretTemplate.TemplateContent != "" { - processedTemplate, err = ProcessLiteralTemplate(templateId, secretTemplate.TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases) - } else { - processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases) - } - - if err != nil { - log.Error().Msgf("unable to process template because %v", err) - - // case: if exit-after-auth is true, it should exit the agent once an error on secret fetching occurs with the appropriate exit code (1) - // previous behavior would exit after 25 sec with status code 0, even if this step errors - if tm.exitAfterAuth { - os.Exit(1) - } - } else { - if (existingEtag != currentEtag) || firstRun { - - tm.WriteTemplateToFile(processedTemplate, &secretTemplate) - existingEtag = currentEtag - - if !firstRun && execCommand != "" { - log.Info().Msgf("executing command: %s", execCommand) - err := ExecuteCommandWithTimeout(execCommand, execTimeout) - - if err != nil { - log.Error().Msgf("unable to execute command because %v", err) - } - - } - if firstRun { - firstRun = false - } - } - } - - // now the idea is we pick the next sleep time in which the one shorter out of - // - polling time - // - first lease that's gonna get expired in the template - firstLeaseExpiry, isValid := tm.dynamicSecretLeases.GetFirstExpiringLeaseTime(templateId) - var waitTime = pollingInterval - if isValid && firstLeaseExpiry.Sub(time.Now()) < pollingInterval { - waitTime = firstLeaseExpiry.Sub(time.Now()) - } - time.Sleep(waitTime) - } else { - // It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render. - time.Sleep(3 * time.Second) - } - } - } - } -} - -// runCmd represents the run command -var agentCmd = &cobra.Command{ - Example: ` - infisical agent - `, - Use: "agent", - Short: "Used to launch a client daemon that streamlines authentication and secret retrieval processes in various environments", - DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { - - log.Info().Msg("starting Infisical agent...") - - configPath, err := cmd.Flags().GetString("config") - if err != nil { - util.HandleError(err, "Unable to parse flag config") - } - - var agentConfigInBytes []byte - - agentConfigInBase64 := os.Getenv("INFISICAL_AGENT_CONFIG_BASE64") - - if agentConfigInBase64 == "" { - data, err := ioutil.ReadFile(configPath) - if err != nil { - if !FileExists(configPath) { - log.Error().Msgf("Unable to locate %s. The provided agent config file path is either missing or incorrect", configPath) - return - } - } - agentConfigInBytes = data - } - - if agentConfigInBase64 != "" { - decodedAgentConfig, err := base64.StdEncoding.DecodeString(agentConfigInBase64) - if err != nil { - log.Error().Msgf("Unable to decode base64 config file because %v", err) - return - } - - agentConfigInBytes = decodedAgentConfig - } - - if !FileExists(configPath) && agentConfigInBase64 == "" { - log.Error().Msgf("No agent config file provided at %v. Please provide a agent config file", configPath) - return - } - - agentConfig, err := ParseAgentConfig(agentConfigInBytes) - if err != nil { - log.Error().Msgf("Unable to prase %s because %v. Please ensure that is follows the Infisical Agent config structure", configPath, err) - return - } - - authMethodValid, authStrategy := util.IsAuthMethodValid(agentConfig.Auth.Type, false) - - if !authMethodValid { - util.PrintErrorMessageAndExit(fmt.Sprintf("The auth method '%s' is not supported.", agentConfig.Auth.Type)) - } - - tokenRefreshNotifier := make(chan bool) - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - filePaths := agentConfig.Sinks - - configBytes, err := yaml.Marshal(agentConfig.Auth.Config) - if err != nil { - log.Error().Msgf("unable to marshal auth config because %v", err) - return - } - - tm := NewAgentManager(NewAgentMangerOptions{ - FileDeposits: filePaths, - Templates: agentConfig.Templates, - AuthConfigBytes: configBytes, - NewAccessTokenNotificationChan: tokenRefreshNotifier, - ExitAfterAuth: agentConfig.Infisical.ExitAfterAuth, - AuthStrategy: authStrategy, - }) - - tm.dynamicSecretLeases = NewDynamicSecretLeaseManager(sigChan) - - go tm.ManageTokenLifecycle() - - for i, template := range agentConfig.Templates { - log.Info().Msgf("template engine started for template %v...", i+1) - go tm.MonitorSecretChanges(template, i, sigChan) - } - - for { - select { - case <-tokenRefreshNotifier: - go tm.WriteTokenToFiles() - case <-sigChan: - log.Info().Msg("agent is gracefully shutting...") - // TODO: check if we are in the middle of writing files to disk - os.Exit(1) - } - } - - }, -} - -func init() { - agentCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { - command.Flags().MarkHidden("domain") - command.Parent().HelpFunc()(command, strings) - }) - agentCmd.Flags().String("config", "agent-config.yaml", "The path to agent config yaml file") - rootCmd.AddCommand(agentCmd) -} diff --git a/cli/packages/cmd/bootstrap.go b/cli/packages/cmd/bootstrap.go deleted file mode 100644 index 7132b634d..000000000 --- a/cli/packages/cmd/bootstrap.go +++ /dev/null @@ -1,277 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "os" - "text/template" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" -) - -// handleK8SecretOutput processes the k8-secret output type by creating a Kubernetes secret -func handleK8SecretOutput(bootstrapResponse api.BootstrapInstanceResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace string) error { - // Create in-cluster config - config, err := rest.InClusterConfig() - if err != nil { - return fmt.Errorf("failed to create in-cluster config: %v", err) - } - - // Create Kubernetes client - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return fmt.Errorf("failed to create Kubernetes client: %v", err) - } - - // Parse and execute the template to render the data/stringData section - tmpl, err := template.New("k8-secret-template").Funcs(template.FuncMap{ - "encodeBase64": func(s string) string { - return base64.StdEncoding.EncodeToString([]byte(s)) - }, - }).Parse(k8SecretTemplate) - - if err != nil { - return fmt.Errorf("failed to parse output template: %v", err) - } - - var renderedDataSection bytes.Buffer - err = tmpl.Execute(&renderedDataSection, bootstrapResponse) - if err != nil { - return fmt.Errorf("failed to execute output template: %v", err) - } - - // Parse the rendered template as JSON to validate it's valid - var dataSection map[string]interface{} - if err := json.Unmarshal(renderedDataSection.Bytes(), &dataSection); err != nil { - return fmt.Errorf("template output is not valid JSON: %v", err) - } - - // Prepare the secret data and stringData maps - secretData := make(map[string][]byte) - secretStringData := make(map[string]string) - - // Process the dataSection to separate data and stringData - if data, exists := dataSection["data"]; exists { - if dataMap, ok := data.(map[string]interface{}); ok { - for key, value := range dataMap { - if strValue, ok := value.(string); ok { - secretData[key] = []byte(strValue) - } - } - } - } - - if stringData, exists := dataSection["stringData"]; exists { - if stringDataMap, ok := stringData.(map[string]interface{}); ok { - for key, value := range stringDataMap { - if strValue, ok := value.(string); ok { - secretStringData[key] = strValue - } - } - } - } - - // Create the Kubernetes secret object - k8sSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: k8SecretName, - Namespace: k8SecretNamespace, - }, - Type: corev1.SecretTypeOpaque, - Data: secretData, - StringData: secretStringData, - } - - ctx := context.Background() - secretsClient := clientset.CoreV1().Secrets(k8SecretNamespace) - - // Check if secret already exists - existingSecret, err := secretsClient.Get(ctx, k8SecretName, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - // Secret doesn't exist, create it - _, err = secretsClient.Create(ctx, k8sSecret, metav1.CreateOptions{}) - if err != nil { - return fmt.Errorf("failed to create Kubernetes secret: %v", err) - } - log.Info().Msgf("Successfully created Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace) - } else { - return fmt.Errorf("failed to check if Kubernetes secret exists: %v", err) - } - } else { - // Secret exists, update it - k8sSecret.ObjectMeta.ResourceVersion = existingSecret.ObjectMeta.ResourceVersion - _, err = secretsClient.Update(ctx, k8sSecret, metav1.UpdateOptions{}) - if err != nil { - return fmt.Errorf("failed to update Kubernetes secret: %v", err) - } - log.Info().Msgf("Successfully updated Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace) - } - - return nil -} - -var bootstrapCmd = &cobra.Command{ - Use: "bootstrap", - Short: "Used to bootstrap your Infisical instance", - DisableFlagsInUseLine: true, - Example: "infisical bootstrap", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - email, _ := cmd.Flags().GetString("email") - if email == "" { - if envEmail, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_EMAIL_NAME); ok { - email = envEmail - } - } - - if email == "" { - log.Error().Msg("email is required") - return - } - - password, _ := cmd.Flags().GetString("password") - if password == "" { - if envPassword, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_PASSWORD_NAME); ok { - password = envPassword - } - } - - if password == "" { - log.Error().Msg("password is required") - return - } - - organization, _ := cmd.Flags().GetString("organization") - if organization == "" { - if envOrganization, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_ORGANIZATION_NAME); ok { - organization = envOrganization - } - } - - if organization == "" { - log.Error().Msg("organization is required") - return - } - - domain, _ := cmd.Flags().GetString("domain") - if domain == "" { - if envDomain, ok := os.LookupEnv("INFISICAL_API_URL"); ok { - domain = envDomain - } - } - - if domain == "" { - log.Error().Msg("domain is required") - return - } - - outputType, err := cmd.Flags().GetString("output") - if err != nil { - log.Error().Msgf("Failed to get output type: %v", err) - return - } - - k8SecretTemplate, err := cmd.Flags().GetString("k8-secret-template") - if err != nil { - log.Error().Msgf("Failed to get k8-secret-template: %v", err) - } - - k8SecretName, err := cmd.Flags().GetString("k8-secret-name") - if err != nil { - log.Error().Msgf("Failed to get k8-secret-name: %v", err) - } - - k8SecretNamespace, err := cmd.Flags().GetString("k8-secret-namespace") - if err != nil { - log.Error().Msgf("Failed to get k8-secret-namespace: %v", err) - } - - if outputType == "k8-secret" { - if k8SecretTemplate == "" { - log.Error().Msg("k8-secret-template is required when using k8-secret output type") - return - } - - if k8SecretName == "" { - log.Error().Msg("k8-secret-name is required when using k8-secret output type") - return - } - - if k8SecretNamespace == "" { - log.Error().Msg("k8-secret-namespace is required when using k8-secret output type") - return - } - } - - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - log.Error().Msgf("Failed to get resty client with custom headers: %v", err) - return - } - - ignoreIfBootstrapped, err := cmd.Flags().GetBool("ignore-if-bootstrapped") - if err != nil { - log.Error().Msgf("Failed to get ignore-if-bootstrapped flag: %v", err) - return - } - - httpClient.SetHeader("Accept", "application/json") - - bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{ - Domain: util.AppendAPIEndpoint(domain), - Email: email, - Password: password, - Organization: organization, - }) - - if err != nil { - if !ignoreIfBootstrapped { - log.Error().Msgf("Failed to bootstrap instance: %v", err) - } - return - } - - if outputType == "k8-secret" { - if err := handleK8SecretOutput(bootstrapResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace); err != nil { - log.Error().Msgf("Failed to handle k8-secret output: %v", err) - return - } - } else { - responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ") - if err != nil { - log.Fatal().Msgf("Failed to convert response to JSON: %v", err) - return - } - - fmt.Println(string(responseJSON)) - } - }, -} - -func init() { - bootstrapCmd.Flags().String("domain", "", "The domain of your self-hosted Infisical instance") - bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin") - bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin") - bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance") - bootstrapCmd.Flags().String("output", "", "The type of output to use for the bootstrap command (json or k8-secret)") - bootstrapCmd.Flags().Bool("ignore-if-bootstrapped", false, "Whether to continue on error if the instance has already been bootstrapped") - bootstrapCmd.Flags().String("k8-secret-template", "{\"data\":{\"token\":\"{{.Identity.Credentials.Token}}\"}}", "The template to use for rendering the Kubernetes secret (entire secret JSON)") - bootstrapCmd.Flags().String("k8-secret-namespace", "", "The namespace to create the Kubernetes secret in") - bootstrapCmd.Flags().String("k8-secret-name", "", "The name of the Kubernetes secret to create") - rootCmd.AddCommand(bootstrapCmd) -} diff --git a/cli/packages/cmd/cmd_test.go b/cli/packages/cmd/cmd_test.go deleted file mode 100644 index f77c6b07b..000000000 --- a/cli/packages/cmd/cmd_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package cmd - -import ( - "testing" - - "github.com/Infisical/infisical-merge/packages/models" -) - -func TestFilterReservedEnvVars(t *testing.T) { - - // some test env vars. - // HOME and PATH are reserved key words and should be filtered out - // XDG_SESSION_ID and LC_CTYPE are reserved key word prefixes and should be filtered out - // The filter function only checks the keys of the env map, so we dont need to set any values - env := map[string]models.SingleEnvironmentVariable{ - "test": {}, - "test2": {}, - "HOME": {}, - "PATH": {}, - "XDG_SESSION_ID": {}, - "LC_CTYPE": {}, - } - - // check to see if there are any reserved key words in secrets to inject - filterReservedEnvVars(env) - - if len(env) != 2 { - t.Errorf("Expected 2 secrets to be returned, got %d", len(env)) - } - if _, ok := env["test"]; !ok { - t.Errorf("Expected test to be returned") - } - if _, ok := env["test2"]; !ok { - t.Errorf("Expected test2 to be returned") - } - if _, ok := env["HOME"]; ok { - t.Errorf("Expected HOME to be filtered out") - } - if _, ok := env["PATH"]; ok { - t.Errorf("Expected PATH to be filtered out") - } - if _, ok := env["XDG_SESSION_ID"]; ok { - t.Errorf("Expected XDG_SESSION_ID to be filtered out") - } - if _, ok := env["LC_CTYPE"]; ok { - t.Errorf("Expected LC_CTYPE to be filtered out") - } - -} diff --git a/cli/packages/cmd/dynamic_secrets.go b/cli/packages/cmd/dynamic_secrets.go deleted file mode 100644 index 0443e7714..000000000 --- a/cli/packages/cmd/dynamic_secrets.go +++ /dev/null @@ -1,676 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "context" - "fmt" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/visualize" - - // "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/util" - // "github.com/Infisical/infisical-merge/packages/visualize" - "github.com/posthog/posthog-go" - "github.com/spf13/cobra" - - infisicalSdk "github.com/infisical/go-sdk" - infisicalSdkModels "github.com/infisical/go-sdk/packages/models" -) - -var dynamicSecretCmd = &cobra.Command{ - Example: `infisical dynamic-secrets`, - Short: "Used to list dynamic secrets", - Use: "dynamic-secrets", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: getDynamicSecretList, -} - -func getDynamicSecretList(cmd *cobra.Command, args []string) { - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectSlug, err := cmd.Flags().GetString("project-slug") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse path flag") - } - - var infisicalToken string - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - - if projectId == "" && projectSlug == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag") - } - projectId = workspaceFile.WorkspaceId - } - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - httpClient.SetAuthToken(infisicalToken) - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - infisicalClient.Auth().SetAccessToken(infisicalToken) - - if projectSlug == "" { - projectDetails, err := api.CallGetProjectById(httpClient, projectId) - if err != nil { - util.HandleError(err, "To fetch project details") - } - projectSlug = projectDetails.Slug - } - - dynamicSecretRootCredentials, err := infisicalClient.DynamicSecrets().List(infisicalSdk.ListDynamicSecretsRootCredentialsOptions{ - ProjectSlug: projectSlug, - SecretPath: secretsPath, - EnvironmentSlug: environmentName, - }) - - if err != nil { - util.HandleError(err, "To fetch dynamic secret root credentials details") - } - - visualize.PrintAllDynamicRootCredentials(dynamicSecretRootCredentials) - Telemetry.CaptureEvent("cli-command:dynamic-secrets", posthog.NewProperties().Set("count", len(dynamicSecretRootCredentials)).Set("version", util.CLI_VERSION)) -} - -var dynamicSecretLeaseCmd = &cobra.Command{ - Example: `lease`, - Short: "Manage leases for dynamic secrets", - Use: "lease", - DisableFlagsInUseLine: true, -} - -var dynamicSecretLeaseCreateCmd = &cobra.Command{ - Example: `lease create "`, - Short: "Used to lease dynamic secret by name", - Use: "create [dynamic-secret]", - DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), - Run: createDynamicSecretLeaseByName, -} - -func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { - dynamicSecretRootCredentialName := args[0] - - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectSlug, err := cmd.Flags().GetString("project-slug") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - ttl, err := cmd.Flags().GetString("ttl") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse path flag") - } - - plainOutput, err := cmd.Flags().GetBool("plain") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - var infisicalToken string - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - - if projectId == "" && projectSlug == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project id with --projectId flag, or pass in project slug with --project-slug flag") - } - projectId = workspaceFile.WorkspaceId - } - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - httpClient.SetAuthToken(infisicalToken) - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - infisicalClient.Auth().SetAccessToken(infisicalToken) - - if projectSlug == "" { - projectDetails, err := api.CallGetProjectById(httpClient, projectId) - if err != nil { - util.HandleError(err, "To fetch project details") - } - projectSlug = projectDetails.Slug - } - - dynamicSecretRootCredential, err := infisicalClient.DynamicSecrets().GetByName(infisicalSdk.GetDynamicSecretRootCredentialByNameOptions{ - DynamicSecretName: dynamicSecretRootCredentialName, - ProjectSlug: projectSlug, - SecretPath: secretsPath, - EnvironmentSlug: environmentName, - }) - - if err != nil { - util.HandleError(err, "To fetch dynamic secret root credentials details") - } - - // for Kubernetes dynamic secrets only - kubernetesNamespace, err := cmd.Flags().GetString("kubernetes-namespace") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - config := map[string]any{} - if kubernetesNamespace != "" { - config["namespace"] = kubernetesNamespace - } - - leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{ - DynamicSecretName: dynamicSecretRootCredential.Name, - ProjectSlug: projectSlug, - TTL: ttl, - SecretPath: secretsPath, - EnvironmentSlug: environmentName, - Config: config, - }) - - if err != nil { - util.HandleError(err, "To lease dynamic secret") - } - - if plainOutput { - for key, value := range leaseCredentials { - if cred, ok := value.(string); ok { - fmt.Printf("%s=%s\n", key, cred) - } - } - } else { - fmt.Println("Dynamic Secret Leasing") - fmt.Printf("Name: %s\n", dynamicSecretRootCredential.Name) - fmt.Printf("Provider: %s\n", dynamicSecretRootCredential.Type) - fmt.Printf("Lease ID: %s\n", leaseDetails.Id) - fmt.Printf("Expire At: %s\n", leaseDetails.ExpireAt.Local().Format("02-Jan-2006 03:04:05 PM")) - visualize.PrintAllDyamicSecretLeaseCredentials(leaseCredentials) - } - - Telemetry.CaptureEvent("cli-command:dynamic-secrets lease", posthog.NewProperties().Set("type", dynamicSecretRootCredential.Type).Set("version", util.CLI_VERSION)) -} - -var dynamicSecretLeaseRenewCmd = &cobra.Command{ - Example: `lease renew "`, - Short: "Used to renew dynamic secret lease by name", - Use: "renew [lease-id]", - DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), - Run: renewDynamicSecretLeaseByName, -} - -func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { - dynamicSecretLeaseId := args[0] - - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectSlug, err := cmd.Flags().GetString("project-slug") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - ttl, err := cmd.Flags().GetString("ttl") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse path flag") - } - - var infisicalToken string - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - - if projectId == "" && projectSlug == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag") - } - projectId = workspaceFile.WorkspaceId - } - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - httpClient.SetAuthToken(infisicalToken) - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - infisicalClient.Auth().SetAccessToken(infisicalToken) - - if projectSlug == "" { - projectDetails, err := api.CallGetProjectById(httpClient, projectId) - if err != nil { - util.HandleError(err, "To fetch project details") - } - projectSlug = projectDetails.Slug - } - - if err != nil { - util.HandleError(err, "To fetch dynamic secret root credentials details") - } - - leaseDetails, err := infisicalClient.DynamicSecrets().Leases().RenewById(infisicalSdk.RenewDynamicSecretLeaseOptions{ - ProjectSlug: projectSlug, - TTL: ttl, - SecretPath: secretsPath, - EnvironmentSlug: environmentName, - LeaseId: dynamicSecretLeaseId, - }) - if err != nil { - util.HandleError(err, "To renew dynamic secret lease") - } - - fmt.Println("Successfully renewed dynamic secret lease") - visualize.PrintAllDynamicSecretLeases([]infisicalSdkModels.DynamicSecretLease{leaseDetails}) - - Telemetry.CaptureEvent("cli-command:dynamic-secrets lease renew", posthog.NewProperties().Set("version", util.CLI_VERSION)) -} - -var dynamicSecretLeaseRevokeCmd = &cobra.Command{ - Example: `lease delete "`, - Short: "Used to delete dynamic secret lease by name", - Use: "delete [lease-id]", - DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), - Run: revokeDynamicSecretLeaseByName, -} - -func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { - dynamicSecretLeaseId := args[0] - - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectSlug, err := cmd.Flags().GetString("project-slug") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse path flag") - } - - var infisicalToken string - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - - if projectId == "" && projectSlug == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag") - } - projectId = workspaceFile.WorkspaceId - } - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - httpClient.SetAuthToken(infisicalToken) - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - infisicalClient.Auth().SetAccessToken(infisicalToken) - - if projectSlug == "" { - projectDetails, err := api.CallGetProjectById(httpClient, projectId) - if err != nil { - util.HandleError(err, "To fetch project details") - } - projectSlug = projectDetails.Slug - } - - if err != nil { - util.HandleError(err, "To fetch dynamic secret root credentials details") - } - - leaseDetails, err := infisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{ - ProjectSlug: projectSlug, - SecretPath: secretsPath, - EnvironmentSlug: environmentName, - LeaseId: dynamicSecretLeaseId, - }) - - if err != nil { - util.HandleError(err, "To revoke dynamic secret lease") - } - - fmt.Println("Successfully revoked dynamic secret lease") - visualize.PrintAllDynamicSecretLeases([]infisicalSdkModels.DynamicSecretLease{leaseDetails}) - - Telemetry.CaptureEvent("cli-command:dynamic-secrets lease revoke", posthog.NewProperties().Set("version", util.CLI_VERSION)) -} - -var dynamicSecretLeaseListCmd = &cobra.Command{ - Example: `lease list "`, - Short: "Used to list leases of a dynamic secret by name", - Use: "list [dynamic-secret]", - DisableFlagsInUseLine: true, - Args: cobra.ExactArgs(1), - Run: listDynamicSecretLeaseByName, -} - -func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { - dynamicSecretRootCredentialName := args[0] - - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectSlug, err := cmd.Flags().GetString("project-slug") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse path flag") - } - - var infisicalToken string - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - - if projectId == "" && projectSlug == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag") - } - projectId = workspaceFile.WorkspaceId - } - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - httpClient.SetAuthToken(infisicalToken) - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - infisicalClient.Auth().SetAccessToken(infisicalToken) - - if projectSlug == "" { - projectDetails, err := api.CallGetProjectById(httpClient, projectId) - if err != nil { - util.HandleError(err, "To fetch project details") - } - projectSlug = projectDetails.Slug - } - - dynamicSecretLeases, err := infisicalClient.DynamicSecrets().Leases().List(infisicalSdk.ListDynamicSecretLeasesOptions{ - DynamicSecretName: dynamicSecretRootCredentialName, - ProjectSlug: projectSlug, - SecretPath: secretsPath, - EnvironmentSlug: environmentName, - }) - - if err != nil { - util.HandleError(err, "To fetch dynamic secret leases list") - } - - visualize.PrintAllDynamicSecretLeases(dynamicSecretLeases) - Telemetry.CaptureEvent("cli-command:dynamic-secrets lease list", posthog.NewProperties().Set("lease-count", len(dynamicSecretLeases)).Set("version", util.CLI_VERSION)) -} - -func init() { - dynamicSecretLeaseCreateCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") - dynamicSecretLeaseCreateCmd.Flags().String("token", "", "Create dynamic secret leases using machine identity access token") - dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth") - dynamicSecretLeaseCreateCmd.Flags().String("project-slug", "", "Manually set the project-slug to create lease in") - dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.") - dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line") - - // Kubernetes specific flags - dynamicSecretLeaseCreateCmd.Flags().String("kubernetes-namespace", "", "The namespace to create the lease in. Only used for Kubernetes dynamic secrets.") - - dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd) - - dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") - dynamicSecretLeaseListCmd.Flags().String("token", "", "Fetch dynamic secret leases machine identity access token") - dynamicSecretLeaseListCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth") - dynamicSecretLeaseListCmd.Flags().String("project-slug", "", "Manually set the project-slug to list leases from") - dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseListCmd) - - dynamicSecretLeaseRenewCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") - dynamicSecretLeaseRenewCmd.Flags().String("token", "", "Renew dynamic secrets machine identity access token") - dynamicSecretLeaseRenewCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth") - dynamicSecretLeaseRenewCmd.Flags().String("project-slug", "", "Manually set the project-slug to renew lease in") - dynamicSecretLeaseRenewCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.") - dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRenewCmd) - - dynamicSecretLeaseRevokeCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from") - dynamicSecretLeaseRevokeCmd.Flags().String("token", "", "Delete dynamic secrets using machine identity access token") - dynamicSecretLeaseRevokeCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth") - dynamicSecretLeaseRevokeCmd.Flags().String("project-slug", "", "Manually set the project-slug to revoke lease from") - dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRevokeCmd) - - dynamicSecretCmd.AddCommand(dynamicSecretLeaseCmd) - - dynamicSecretCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - dynamicSecretCmd.Flags().String("projectId", "", "Manually set the projectId to fetch dynamic-secret when using machine identity based auth") - dynamicSecretCmd.Flags().String("project-slug", "", "Manually set the project-slug to fetch dynamic-secret from") - dynamicSecretCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") - dynamicSecretCmd.Flags().String("path", "/", "get dynamic secret within a folder path") - rootCmd.AddCommand(dynamicSecretCmd) -} diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go deleted file mode 100644 index b872b0e61..000000000 --- a/cli/packages/cmd/export.go +++ /dev/null @@ -1,240 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "encoding/csv" - "encoding/json" - "fmt" - "os" - "strings" - - "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "gopkg.in/yaml.v2" -) - -const ( - FormatDotenv string = "dotenv" - FormatJson string = "json" - FormatCSV string = "csv" - FormatYaml string = "yaml" - FormatDotEnvExport string = "dotenv-export" -) - -// exportCmd represents the export command -var exportCmd = &cobra.Command{ - Use: "export", - Short: "Used to export environment variables to a file", - DisableFlagsInUseLine: true, - Example: "infisical export --env=prod --format=json > secrets.json", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - shouldExpandSecrets, err := cmd.Flags().GetBool("expand") - if err != nil { - util.HandleError(err) - } - - includeImports, err := cmd.Flags().GetBool("include-imports") - if err != nil { - util.HandleError(err) - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err) - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - format, err := cmd.Flags().GetString("format") - if err != nil { - util.HandleError(err) - } - - templatePath, err := cmd.Flags().GetString("template") - if err != nil { - util.HandleError(err) - } - - secretOverriding, err := cmd.Flags().GetBool("secret-overriding") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - tagSlugs, err := cmd.Flags().GetString("tags") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - request := models.GetAllSecretsParameters{ - Environment: environmentName, - TagSlugs: tagSlugs, - WorkspaceId: projectId, - SecretsPath: secretsPath, - IncludeImport: includeImports, - ExpandSecretReferences: shouldExpandSecrets, - } - - if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { - request.InfisicalToken = token.Token - } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - request.UniversalAuthAccessToken = token.Token - } - - if templatePath != "" { - sigChan := make(chan os.Signal, 1) - dynamicSecretLeases := NewDynamicSecretLeaseManager(sigChan) - newEtag := "" - - accessToken := "" - if token != nil { - accessToken = token.Token - } else { - log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details") - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err) - } - accessToken = loggedInUserDetails.UserCredentials.JTWToken - } - - processedTemplate, err := ProcessTemplate(1, templatePath, nil, accessToken, "", &newEtag, dynamicSecretLeases) - if err != nil { - util.HandleError(err) - } - fmt.Print(processedTemplate.String()) - return - } - - secrets, err := util.GetAllEnvironmentVariables(request, "") - if err != nil { - util.HandleError(err, "Unable to fetch secrets") - } - - if secretOverriding { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL) - } else { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED) - } - - var output string - secrets = util.FilterSecretsByTag(secrets, tagSlugs) - secrets = util.SortSecretsByKeys(secrets) - - output, err = formatEnvs(secrets, format) - if err != nil { - util.HandleError(err) - } - - fmt.Print(output) - - // Telemetry.CaptureEvent("cli-command:export", posthog.NewProperties().Set("secretsCount", len(secrets)).Set("version", util.CLI_VERSION)) - }, -} - -func init() { - rootCmd.AddCommand(exportCmd) - exportCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") - exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") - exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") - exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") - exportCmd.Flags().Bool("include-imports", true, "Imported linked secrets") - exportCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs") - exportCmd.Flags().String("projectId", "", "manually set the projectId to export secrets from") - exportCmd.Flags().String("path", "/", "get secrets within a folder path") - exportCmd.Flags().String("template", "", "The path to the template file used to render secrets") -} - -// Format according to the format flag -func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string, error) { - switch strings.ToLower(format) { - case FormatDotenv: - return formatAsDotEnv(envs), nil - case FormatDotEnvExport: - return formatAsDotEnvExport(envs), nil - case FormatJson: - return formatAsJson(envs), nil - case FormatCSV: - return formatAsCSV(envs), nil - case FormatYaml: - return formatAsYaml(envs) - default: - return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport}) - } -} - -// Format environment variables as a CSV file -func formatAsCSV(envs []models.SingleEnvironmentVariable) string { - csvString := &strings.Builder{} - writer := csv.NewWriter(csvString) - writer.Write([]string{"Key", "Value"}) - for _, env := range envs { - writer.Write([]string{env.Key, env.Value}) - } - writer.Flush() - return csvString.String() -} - -// Format environment variables as a dotenv file -func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string { - var dotenv string - for _, env := range envs { - dotenv += fmt.Sprintf("%s='%s'\n", env.Key, env.Value) - } - return dotenv -} - -// Format environment variables as a dotenv file with export at the beginning -func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string { - var dotenv string - for _, env := range envs { - dotenv += fmt.Sprintf("export %s='%s'\n", env.Key, env.Value) - } - return dotenv -} - -func formatAsYaml(envs []models.SingleEnvironmentVariable) (string, error) { - m := make(map[string]string) - for _, env := range envs { - m[env.Key] = env.Value - } - - yamlBytes, err := yaml.Marshal(m) - if err != nil { - return "", fmt.Errorf("failed to format environment variables as YAML: %w", err) - } - - return string(yamlBytes), nil -} - -// Format environment variables as a JSON file -func formatAsJson(envs []models.SingleEnvironmentVariable) string { - // Dump as a json array - json, err := json.Marshal(envs) - if err != nil { - log.Err(err).Msgf("Unable to marshal environment variables to JSON") - return "" - } - return string(json) -} diff --git a/cli/packages/cmd/export_test.go b/cli/packages/cmd/export_test.go deleted file mode 100644 index 1be0a7ed2..000000000 --- a/cli/packages/cmd/export_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package cmd - -import ( - "testing" - - "github.com/Infisical/infisical-merge/packages/models" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v2" -) - -func TestFormatAsYaml(t *testing.T) { - tests := []struct { - name string - input []models.SingleEnvironmentVariable - expected string - }{ - { - name: "Empty input", - input: []models.SingleEnvironmentVariable{}, - expected: "{}\n", - }, - { - name: "Single environment variable", - input: []models.SingleEnvironmentVariable{ - {Key: "KEY1", Value: "VALUE1"}, - }, - expected: "KEY1: VALUE1\n", - }, - { - name: "Multiple environment variables", - input: []models.SingleEnvironmentVariable{ - {Key: "KEY1", Value: "VALUE1"}, - {Key: "KEY2", Value: "VALUE2"}, - {Key: "KEY3", Value: "VALUE3"}, - }, - expected: "KEY1: VALUE1\nKEY2: VALUE2\nKEY3: VALUE3\n", - }, - { - name: "Overwriting duplicate keys", - input: []models.SingleEnvironmentVariable{ - {Key: "KEY1", Value: "VALUE1"}, - {Key: "KEY1", Value: "VALUE2"}, - }, - expected: "KEY1: VALUE2\n", - }, - { - name: "Special characters in values", - input: []models.SingleEnvironmentVariable{ - {Key: "KEY1", Value: "Value with spaces"}, - {Key: "KEY2", Value: "Value:with:colons"}, - {Key: "KEY3", Value: "Value\nwith\nnewlines"}, - }, - expected: "KEY1: Value with spaces\nKEY2: Value:with:colons\nKEY3: |-\n Value\n with\n newlines\n", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := formatAsYaml(tt.input) - assert.NoError(t, err) - - // Compare the result with the expected output - assert.Equal(t, tt.expected, result) - - // Additionally, parse the result back into a map to ensure it's valid YAML - var resultMap map[string]string - err = yaml.Unmarshal([]byte(result), &resultMap) - assert.NoError(t, err) - - // Create an expected map from the input - expectedMap := make(map[string]string) - for _, env := range tt.input { - expectedMap[env.Key] = env.Value - } - - assert.Equal(t, expectedMap, resultMap) - }) - } -} diff --git a/cli/packages/cmd/folder.go b/cli/packages/cmd/folder.go deleted file mode 100644 index b59652191..000000000 --- a/cli/packages/cmd/folder.go +++ /dev/null @@ -1,209 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - - "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/Infisical/infisical-merge/packages/visualize" - "github.com/posthog/posthog-go" - "github.com/spf13/cobra" -) - -var folderCmd = &cobra.Command{ - Use: "folders", - Short: "Create, delete, and list folders", - DisableFlagsInUseLine: true, - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, -} - -var getCmd = &cobra.Command{ - Use: "get", - Short: "Get folders in a directory", - Run: func(cmd *cobra.Command, args []string) { - - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - foldersPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - request := models.GetAllFoldersParameters{ - Environment: environmentName, - WorkspaceId: projectId, - FoldersPath: foldersPath, - } - - if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { - request.InfisicalToken = token.Token - } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - request.UniversalAuthAccessToken = token.Token - } - - folders, err := util.GetAllFolders(request) - if err != nil { - util.HandleError(err, "Unable to get folders") - } - - visualize.PrintAllFoldersDetails(folders, foldersPath) - Telemetry.CaptureEvent("cli-command:folders get", posthog.NewProperties().Set("folderCount", len(folders)).Set("version", util.CLI_VERSION)) - }, -} - -var createCmd = &cobra.Command{ - Use: "create", - Short: "Create a folder", - Run: func(cmd *cobra.Command, args []string) { - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - folderPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - folderName, err := cmd.Flags().GetString("name") - if err != nil { - util.HandleError(err, "Unable to parse name flag") - } - - if folderName == "" { - util.HandleError(errors.New("invalid folder name, folder name cannot be empty")) - } - - if err != nil { - util.HandleError(err, "Unable to get workspace file") - } - - if projectId == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - - projectId = workspaceFile.WorkspaceId - } - - params := models.CreateFolderParameters{ - FolderName: folderName, - Environment: environmentName, - FolderPath: folderPath, - WorkspaceId: projectId, - } - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - params.InfisicalToken = token.Token - } - - _, err = util.CreateFolder(params) - if err != nil { - util.HandleError(err, "Unable to create folder") - } - - util.PrintSuccessMessage(fmt.Sprintf("folder named `%s` created in path %s", folderName, folderPath)) - - Telemetry.CaptureEvent("cli-command:folders create", posthog.NewProperties().Set("version", util.CLI_VERSION)) - }, -} - -var deleteCmd = &cobra.Command{ - Use: "delete", - Short: "Delete a folder", - Run: func(cmd *cobra.Command, args []string) { - - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - folderPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - folderName, err := cmd.Flags().GetString("name") - if err != nil { - util.HandleError(err, "Unable to parse name flag") - } - - if folderName == "" { - util.HandleError(errors.New("invalid folder name, folder name cannot be empty")) - } - - if projectId == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - - projectId = workspaceFile.WorkspaceId - } - - params := models.DeleteFolderParameters{ - FolderName: folderName, - WorkspaceId: projectId, - Environment: environmentName, - FolderPath: folderPath, - } - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - params.InfisicalToken = token.Token - } - - _, err = util.DeleteFolder(params) - if err != nil { - util.HandleError(err, "Unable to delete folder") - } - - util.PrintSuccessMessage(fmt.Sprintf("folder named `%s` deleted in path %s", folderName, folderPath)) - - Telemetry.CaptureEvent("cli-command:folders delete", posthog.NewProperties().Set("version", util.CLI_VERSION)) - }, -} diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go deleted file mode 100644 index abc4d6949..000000000 --- a/cli/packages/cmd/gateway.go +++ /dev/null @@ -1,318 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "os" - "os/exec" - "os/signal" - "runtime" - "sync/atomic" - "syscall" - "time" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/gateway" - "github.com/Infisical/infisical-merge/packages/util" - infisicalSdk "github.com/infisical/go-sdk" - "github.com/pkg/errors" - "github.com/posthog/posthog-go" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -func getInfisicalSdkInstance(cmd *cobra.Command) (infisicalSdk.InfisicalClientInterface, context.CancelFunc, error) { - - ctx, cancel := context.WithCancel(cmd.Context()) - infisicalClient := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - }) - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - cancel() - return nil, nil, err - } - - // if the --token param is set, we use it directly for authentication - if token != nil { - infisicalClient.Auth().SetAccessToken(token.Token) - return infisicalClient, cancel, nil - } - - // if the --token param is not set, we use the auth-method flag to determine the authentication method, and perform the appropriate login flow based on that - authMethod, err := util.GetCmdFlagOrEnv(cmd, "auth-method", []string{util.INFISICAL_AUTH_METHOD_NAME}) - - if err != nil { - cancel() - return nil, nil, err - } - - authMethodValid, strategy := util.IsAuthMethodValid(authMethod, false) - if !authMethodValid { - util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", authMethod)) - } - - sdkAuthenticator := util.NewSdkAuthenticator(infisicalClient, cmd) - - authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){ - util.AuthStrategy.UNIVERSAL_AUTH: sdkAuthenticator.HandleUniversalAuthLogin, - util.AuthStrategy.KUBERNETES_AUTH: sdkAuthenticator.HandleKubernetesAuthLogin, - util.AuthStrategy.AZURE_AUTH: sdkAuthenticator.HandleAzureAuthLogin, - util.AuthStrategy.GCP_ID_TOKEN_AUTH: sdkAuthenticator.HandleGcpIdTokenAuthLogin, - util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin, - util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin, - util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin, - util.AuthStrategy.JWT_AUTH: sdkAuthenticator.HandleJwtAuthLogin, - } - - _, err = authStrategies[strategy]() - - if err != nil { - cancel() - return nil, nil, err - } - - return infisicalClient, cancel, nil -} - -var gatewayCmd = &cobra.Command{ - Use: "gateway", - Short: "Run the Infisical gateway or manage its systemd service", - Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.", - Example: `infisical gateway --token= - sudo infisical gateway install --token= --domain=`, - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - - infisicalClient, cancelSdk, err := getInfisicalSdkInstance(cmd) - if err != nil { - util.HandleError(err, "unable to get infisical client") - } - defer cancelSdk() - - var accessToken atomic.Value - accessToken.Store(infisicalClient.Auth().GetAccessToken()) - - if accessToken.Load().(string) == "" { - util.HandleError(errors.New("no access token found")) - } - - Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION)) - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - sigStopCh := make(chan bool, 1) - - ctx, cancelCmd := context.WithCancel(cmd.Context()) - defer cancelCmd() - - go func() { - <-sigCh - close(sigStopCh) - cancelCmd() - cancelSdk() - - // If we get a second signal, force exit - <-sigCh - log.Warn().Msgf("Force exit triggered") - os.Exit(1) - }() - - var gatewayInstance *gateway.Gateway - - // Token refresh goroutine - runs every 10 seconds - go func() { - tokenRefreshTicker := time.NewTicker(10 * time.Second) - defer tokenRefreshTicker.Stop() - - for { - select { - case <-tokenRefreshTicker.C: - if ctx.Err() != nil { - return - } - - newToken := infisicalClient.Auth().GetAccessToken() - if newToken != "" && newToken != accessToken.Load().(string) { - accessToken.Store(newToken) - if gatewayInstance != nil { - gatewayInstance.UpdateIdentityAccessToken(newToken) - } - } - - case <-ctx.Done(): - return - } - } - }() - - // Main gateway retry loop with proper context handling - retryTicker := time.NewTicker(5 * time.Second) - defer retryTicker.Stop() - - for { - if ctx.Err() != nil { - log.Info().Msg("Shutting down gateway") - return - } - gatewayInstance, err := gateway.NewGateway(accessToken.Load().(string)) - if err != nil { - util.HandleError(err) - } - - if err = gatewayInstance.ConnectWithRelay(); err != nil { - if ctx.Err() != nil { - log.Info().Msg("Shutting down gateway") - return - } - - log.Error().Msgf("Gateway connection error with relay: %s", err) - log.Info().Msg("Retrying connection in 5 seconds...") - select { - case <-retryTicker.C: - continue - case <-ctx.Done(): - log.Info().Msg("Shutting down gateway") - return - } - } - - err = gatewayInstance.Listen(ctx) - if ctx.Err() != nil { - log.Info().Msg("Gateway shutdown complete") - return - } - log.Error().Msgf("Gateway listen error: %s", err) - log.Info().Msg("Retrying connection in 5 seconds...") - select { - case <-retryTicker.C: - continue - case <-ctx.Done(): - log.Info().Msg("Shutting down gateway") - return - } - } - }, -} - -var gatewayInstallCmd = &cobra.Command{ - Use: "install", - Short: "Install and enable systemd service for the gateway (requires sudo)", - Long: "Install and enable systemd service for the gateway. Must be run with sudo on Linux.", - Example: "sudo infisical gateway install --token= --domain=", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - if runtime.GOOS != "linux" { - util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux")) - } - - if os.Geteuid() != 0 { - util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges")) - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if token == nil { - util.HandleError(errors.New("Token not found")) - } - - domain, err := cmd.Flags().GetString("domain") - if err != nil { - util.HandleError(err, "Unable to parse domain flag") - } - - if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil { - util.HandleError(err, "Failed to install systemd service") - } - - enableCmd := exec.Command("systemctl", "enable", "infisical-gateway") - if err := enableCmd.Run(); err != nil { - util.HandleError(err, "Failed to enable systemd service") - } - - log.Info().Msg("Successfully installed and enabled infisical-gateway service") - log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway") - }, -} - -var gatewayUninstallCmd = &cobra.Command{ - Use: "uninstall", - Short: "Uninstall and remove systemd service for the gateway (requires sudo)", - Long: "Uninstall and remove systemd service for the gateway. Must be run with sudo on Linux.", - Example: "sudo infisical gateway uninstall", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - if runtime.GOOS != "linux" { - util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux")) - } - - if os.Geteuid() != 0 { - util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges")) - } - - if err := gateway.UninstallGatewaySystemdService(); err != nil { - util.HandleError(err, "Failed to uninstall systemd service") - } - }, -} - -var gatewayRelayCmd = &cobra.Command{ - Example: `infisical gateway relay`, - Short: "Used to run infisical gateway relay", - Use: "relay", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - relayConfigFilePath, err := cmd.Flags().GetString("config") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if relayConfigFilePath == "" { - util.HandleError(errors.New("Missing config file")) - } - - gatewayRelay, err := gateway.NewGatewayRelay(relayConfigFilePath) - if err != nil { - util.HandleError(err, "Failed to initialize gateway") - } - err = gatewayRelay.Run() - if err != nil { - util.HandleError(err, "Failed to start gateway") - } - }, -} - -func init() { - gatewayCmd.Flags().String("token", "", "connect with Infisical using machine identity access token. if not provided, you must set the auth-method flag") - - gatewayCmd.Flags().String("auth-method", "", "login method [universal-auth, kubernetes, azure, gcp-id-token, gcp-iam, aws-iam, oidc-auth]. if not provided, you must set the token flag") - - gatewayCmd.Flags().String("client-id", "", "client id for universal auth") - gatewayCmd.Flags().String("client-secret", "", "client secret for universal auth") - - gatewayCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods") - gatewayCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth") - gatewayCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") - - gatewayCmd.Flags().String("jwt", "", "JWT for jwt-based auth methods [oidc-auth, jwt-auth]") - - gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token") - gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance") - - gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path") - - gatewayCmd.AddCommand(gatewayInstallCmd) - gatewayCmd.AddCommand(gatewayUninstallCmd) - gatewayCmd.AddCommand(gatewayRelayCmd) - rootCmd.AddCommand(gatewayCmd) -} diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go deleted file mode 100644 index 2ef555a82..000000000 --- a/cli/packages/cmd/init.go +++ /dev/null @@ -1,195 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "encoding/json" - "fmt" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/manifoldco/promptui" - "github.com/posthog/posthog-go" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -// runCmd represents the run command -var initCmd = &cobra.Command{ - Use: "init", - Short: "Used to connect your local project with Infisical project", - DisableFlagsInUseLine: true, - Example: "infisical init", - Args: cobra.ExactArgs(0), - PreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - }, - Run: func(cmd *cobra.Command, args []string) { - if util.WorkspaceConfigFileExistsInCurrentPath() { - shouldOverride, err := shouldOverrideWorkspacePrompt() - if err != nil { - log.Error().Msg("Unable to parse your answer") - log.Debug().Err(err) - return - } - - if !shouldOverride { - return - } - } - - userCreds, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to get your login details") - } - - if userCreds.LoginExpired { - userCreds = util.EstablishUserLoginSession() - } - - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken) - - organizationResponse, err := api.CallGetAllOrganizations(httpClient) - if err != nil { - util.HandleError(err, "Unable to pull organizations that belong to you") - } - - organizations := organizationResponse.Organizations - - organizationNames := util.GetOrganizationsNameList(organizationResponse) - - prompt := promptui.Select{ - Label: "Which Infisical organization would you like to select a project from?", - Items: organizationNames, - Size: 7, - } - - index, _, err := prompt.Run() - if err != nil { - util.HandleError(err) - } - - selectedOrganization := organizations[index] - - tokenResponse, err := api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrganization.ID}) - if tokenResponse.MfaEnabled { - i := 1 - for i < 6 { - mfaVerifyCode := askForMFACode(tokenResponse.MfaMethod) - - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - httpClient.SetAuthToken(tokenResponse.Token) - verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ - Email: userCreds.UserCredentials.Email, - MFAToken: mfaVerifyCode, - MFAMethod: tokenResponse.MfaMethod, - }) - if requestError != nil { - util.HandleError(err) - break - } else if mfaErrorResponse != nil { - if mfaErrorResponse.Context.Code == "mfa_invalid" { - msg := fmt.Sprintf("Incorrect, verification code. You have %v attempts left", 5-i) - fmt.Println(msg) - if i == 5 { - util.PrintErrorMessageAndExit("No tries left, please try again in a bit") - break - } - } - - if mfaErrorResponse.Context.Code == "mfa_expired" { - util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again") - break - } - i++ - } else { - httpClient.SetAuthToken(verifyMFAresponse.Token) - tokenResponse, err = api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrganization.ID}) - break - } - } - } - - if err != nil { - util.HandleError(err, "Unable to select organization") - } - - // set the config jwt token to the new token - userCreds.UserCredentials.JTWToken = tokenResponse.Token - err = util.StoreUserCredsInKeyRing(&userCreds.UserCredentials) - httpClient.SetAuthToken(tokenResponse.Token) - - if err != nil { - util.HandleError(err, "Unable to store your user credentials") - } - - workspaceResponse, err := api.CallGetAllWorkSpacesUserBelongsTo(httpClient) - if err != nil { - util.HandleError(err, "Unable to pull projects that belong to you") - } - - filteredWorkspaces, workspaceNames := util.GetWorkspacesInOrganization(workspaceResponse, selectedOrganization.ID) - - prompt = promptui.Select{ - Label: "Which of your Infisical projects would you like to connect this project to?", - Items: workspaceNames, - Size: 7, - } - - index, _, err = prompt.Run() - if err != nil { - util.HandleError(err) - } - - err = writeWorkspaceFile(filteredWorkspaces[index]) - if err != nil { - util.HandleError(err) - } - - Telemetry.CaptureEvent("cli-command:init", posthog.NewProperties().Set("version", util.CLI_VERSION)) - - }, -} - -func init() { - rootCmd.AddCommand(initCmd) -} - -func writeWorkspaceFile(selectedWorkspace models.Workspace) error { - workspaceFileToSave := models.WorkspaceConfigFile{ - WorkspaceId: selectedWorkspace.ID, - } - - marshalledWorkspaceFile, err := json.MarshalIndent(workspaceFileToSave, "", " ") - if err != nil { - return err - } - - err = util.WriteToFile(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, 0600) - if err != nil { - return err - } - - return nil -} - -func shouldOverrideWorkspacePrompt() (bool, error) { - prompt := promptui.Select{ - Label: "A workspace config file already exists here. Would you like to override? Select[Yes/No]", - Items: []string{"No", "Yes"}, - } - _, result, err := prompt.Run() - if err != nil { - return false, err - } - return result == "Yes", nil -} diff --git a/cli/packages/cmd/kmip.go b/cli/packages/cmd/kmip.go deleted file mode 100644 index 91335d122..000000000 --- a/cli/packages/cmd/kmip.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "fmt" - - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/util" - kmip "github.com/infisical/infisical-kmip" - "github.com/spf13/cobra" -) - -var kmipCmd = &cobra.Command{ - Example: `infisical kmip`, - Short: "Used to manage KMIP servers", - Use: "kmip", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, -} - -var kmipStartCmd = &cobra.Command{ - Example: `infisical kmip start`, - Short: "Used to start a KMIP server", - Use: "start", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: startKmipServer, -} - -func startKmipServer(cmd *cobra.Command, args []string) { - listenAddr, err := cmd.Flags().GetString("listen-address") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - identityAuthMethod, err := cmd.Flags().GetString("identity-auth-method") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - authMethodValid, strategy := util.IsAuthMethodValid(identityAuthMethod, false) - if !authMethodValid { - util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", identityAuthMethod)) - } - - var identityClientId string - var identityClientSecret string - - if strategy == util.AuthStrategy.UNIVERSAL_AUTH { - identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME}) - - if err != nil { - util.HandleError(err, "Unable to parse identity client ID") - } - - identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME}) - if err != nil { - util.HandleError(err, "Unable to parse identity client secret") - } - } else { - util.PrintErrorMessageAndExit(fmt.Sprintf("Unsupported login method: %s", identityAuthMethod)) - } - - serverName, err := cmd.Flags().GetString("server-name") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - certificateTTL, err := cmd.Flags().GetString("certificate-ttl") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - hostnamesOrIps, err := cmd.Flags().GetString("hostnames-or-ips") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - kmip.StartServer(kmip.ServerConfig{ - Addr: listenAddr, - InfisicalBaseAPIURL: config.INFISICAL_URL, - IdentityClientId: identityClientId, - IdentityClientSecret: identityClientSecret, - ServerName: serverName, - CertificateTTL: certificateTTL, - HostnamesOrIps: hostnamesOrIps, - }) -} - -func init() { - kmipStartCmd.Flags().String("listen-address", "localhost:5696", "The address for the KMIP server to listen on. Defaults to localhost:5696") - kmipStartCmd.Flags().String("identity-auth-method", string(util.AuthStrategy.UNIVERSAL_AUTH), "The auth method to use for authenticating the machine identity. Defaults to universal-auth.") - kmipStartCmd.Flags().String("identity-client-id", "", "Universal auth client ID of machine identity") - kmipStartCmd.Flags().String("identity-client-secret", "", "Universal auth client secret of machine identity") - kmipStartCmd.Flags().String("server-name", "kmip-server", "The name of the KMIP server") - kmipStartCmd.Flags().String("certificate-ttl", "1y", "The TTL duration for the server certificate") - kmipStartCmd.Flags().String("hostnames-or-ips", "", "Comma-separated list of hostnames or IPs") - - kmipCmd.AddCommand(kmipStartCmd) - rootCmd.AddCommand(kmipCmd) -} diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go deleted file mode 100644 index fd3ce1569..000000000 --- a/cli/packages/cmd/login.go +++ /dev/null @@ -1,1022 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "context" - "encoding/base64" - "encoding/hex" - "encoding/json" - "os" - "runtime" - "slices" - "strings" - "time" - - "errors" - "fmt" - "net" - "net/http" - "net/url" - "regexp" - - browser "github.com/pkg/browser" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/crypto" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/srp" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/fatih/color" - "github.com/manifoldco/promptui" - "github.com/posthog/posthog-go" - "github.com/rs/cors" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "golang.org/x/crypto/argon2" - "golang.org/x/term" - - infisicalSdk "github.com/infisical/go-sdk" -) - -type params struct { - memory uint32 - iterations uint32 - parallelism uint8 - saltLength uint32 - keyLength uint32 -} - -func formatAuthMethod(authMethod string) string { - return strings.ReplaceAll(authMethod, "-", " ") -} - -const ADD_USER = "Add a new account login" -const REPLACE_USER = "Override current logged in user" -const EXIT_USER_MENU = "Exit" -const QUIT_BROWSER_LOGIN = "q" - -// loginCmd represents the login command -var loginCmd = &cobra.Command{ - Use: "login", - Short: "Login into your Infisical account", - DisableFlagsInUseLine: true, - PreRunE: func(cmd *cobra.Command, args []string) error { - // daniel: oidc-jwt is deprecated in favor of `jwt`. we backfill the `jwt` flag with the value of `oidc-jwt` if it's set. - if cmd.Flags().Changed("oidc-jwt") && !cmd.Flags().Changed("jwt") { - oidcJWT, err := cmd.Flags().GetString("oidc-jwt") - if err != nil { - return err - } - - err = cmd.Flags().Set("jwt", oidcJWT) - if err != nil { - return err - } - } - return nil - }, - Run: func(cmd *cobra.Command, args []string) { - presetDomain := config.INFISICAL_URL - - clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains") - if err != nil { - util.HandleError(err) - } - - if clearSelfHostedDomains { - infisicalConfig, err := util.GetConfigFile() - if err != nil { - util.HandleError(err) - } - - infisicalConfig.Domains = []string{} - err = util.WriteConfigFile(&infisicalConfig) - - if err != nil { - util.HandleError(err) - } - - fmt.Println("Cleared all self-hosted domains from the config file") - return - } - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - - loginMethod, err := cmd.Flags().GetString("method") - if err != nil { - util.HandleError(err) - } - plainOutput, err := cmd.Flags().GetBool("plain") - if err != nil { - util.HandleError(err) - } - - authMethodValid, strategy := util.IsAuthMethodValid(loginMethod, true) - if !authMethodValid { - util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", loginMethod)) - } - - // standalone user auth - if loginMethod == "user" { - currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - // if the key can't be found or there is an error getting current credentials from key ring, allow them to override - if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) { - log.Debug().Err(err) - } else if err != nil { - util.HandleError(err) - } - - if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 { - shouldOverride, err := userLoginMenu(currentLoggedInUserDetails.UserCredentials.Email) - if err != nil { - util.HandleError(err) - } - - if !shouldOverride { - return - } - } - - usePresetDomain, err := usePresetDomain(presetDomain) - - if err != nil { - util.HandleError(err) - } - - //override domain - domainQuery := true - if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && - config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) && - config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) && - !usePresetDomain { - overrideDomain, err := DomainOverridePrompt() - if err != nil { - util.HandleError(err) - } - - //if not override set INFISICAL_URL to exported var - //set domainQuery to false - if !overrideDomain && !usePresetDomain { - domainQuery = false - config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE) - config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api")) - } - - } - - //prompt user to select domain between Infisical cloud and self-hosting - if domainQuery && !usePresetDomain { - err = askForDomain() - if err != nil { - util.HandleError(err, "Unable to parse domain url") - } - } - var userCredentialsToBeStored models.UserCredentials - - interactiveLogin := false - if cmd.Flags().Changed("interactive") { - interactiveLogin = true - cliDefaultLogin(&userCredentialsToBeStored) - } - - //call browser login function - if !interactiveLogin { - userCredentialsToBeStored, err = browserCliLogin() - if err != nil { - fmt.Printf("Login via browser failed. %s", err.Error()) - //default to cli login on error - cliDefaultLogin(&userCredentialsToBeStored) - } - } - - err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored) - if err != nil { - log.Error().Msgf("Unable to store your credentials in system vault") - log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq") - log.Debug().Err(err) - //return here - util.HandleError(err) - } - - err = util.WriteInitalConfig(&userCredentialsToBeStored) - if err != nil { - util.HandleError(err, "Unable to write write to Infisical Config file. Please try again") - } - - // clear backed up secrets from prev account - util.DeleteBackupSecrets() - - whilte := color.New(color.FgGreen) - boldWhite := whilte.Add(color.Bold) - time.Sleep(time.Second * 1) - boldWhite.Printf(">>>> Welcome to Infisical!") - boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email) - - plainBold := color.New(color.Bold) - - plainBold.Println("\nQuick links") - fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage") - fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack") - Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION)) - } else { - - sdkAuthenticator := util.NewSdkAuthenticator(infisicalClient, cmd) - - authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){ - util.AuthStrategy.UNIVERSAL_AUTH: sdkAuthenticator.HandleUniversalAuthLogin, - util.AuthStrategy.KUBERNETES_AUTH: sdkAuthenticator.HandleKubernetesAuthLogin, - util.AuthStrategy.AZURE_AUTH: sdkAuthenticator.HandleAzureAuthLogin, - util.AuthStrategy.GCP_ID_TOKEN_AUTH: sdkAuthenticator.HandleGcpIdTokenAuthLogin, - util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin, - util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin, - util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin, - util.AuthStrategy.JWT_AUTH: sdkAuthenticator.HandleJwtAuthLogin, - } - - credential, err := authStrategies[strategy]() - - if err != nil { - euErrorMessage := "" - if strings.HasPrefix(config.INFISICAL_URL, util.INFISICAL_DEFAULT_US_URL) { - euErrorMessage = fmt.Sprintf("\nIf you are using the Infisical Cloud Europe Region, please switch to it by using the \"--domain %s\" flag.", util.INFISICAL_DEFAULT_EU_URL) - } - util.HandleError(fmt.Errorf("unable to authenticate with %s [err=%v].%s", formatAuthMethod(loginMethod), err, euErrorMessage)) - } - - if plainOutput { - fmt.Println(credential.AccessToken) - return - } - - boldGreen := color.New(color.FgGreen).Add(color.Bold) - boldPlain := color.New(color.Bold) - time.Sleep(time.Second * 1) - boldGreen.Printf(">>>> Successfully authenticated with %s!\n\n", formatAuthMethod(loginMethod)) - boldPlain.Printf("Access Token:\n%v", credential.AccessToken) - - plainBold := color.New(color.Bold) - plainBold.Println("\n\nYou can use this access token to authenticate through other commands in the CLI.") - - } - }, -} - -func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { - email, password, err := askForLoginCredentials() - if err != nil { - util.HandleError(err, "Unable to parse email and password for authentication") - } - - loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) - if err != nil { - fmt.Println("Unable to authenticate with the provided credentials, please try again") - log.Debug().Err(err) - //return here - util.HandleError(err) - } - - if loginTwoResponse.MfaEnabled { - i := 1 - for i < 6 { - mfaVerifyCode := askForMFACode("email") - - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - httpClient.SetAuthToken(loginTwoResponse.Token) - verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ - Email: email, - MFAToken: mfaVerifyCode, - }) - - if requestError != nil { - util.HandleError(err) - break - } else if mfaErrorResponse != nil { - if mfaErrorResponse.Context.Code == "mfa_invalid" { - msg := fmt.Sprintf("Incorrect, verification code. You have %v attempts left", 5-i) - fmt.Println(msg) - if i == 5 { - util.PrintErrorMessageAndExit("No tries left, please try again in a bit") - break - } - } - - if mfaErrorResponse.Context.Code == "mfa_expired" { - util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again") - break - } - i++ - } else { - loginTwoResponse.EncryptedPrivateKey = verifyMFAresponse.EncryptedPrivateKey - loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion - loginTwoResponse.Iv = verifyMFAresponse.Iv - loginTwoResponse.ProtectedKey = verifyMFAresponse.ProtectedKey - loginTwoResponse.ProtectedKeyIV = verifyMFAresponse.ProtectedKeyIV - loginTwoResponse.ProtectedKeyTag = verifyMFAresponse.ProtectedKeyTag - loginTwoResponse.PublicKey = verifyMFAresponse.PublicKey - loginTwoResponse.Tag = verifyMFAresponse.Tag - loginTwoResponse.Token = verifyMFAresponse.Token - loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion - - break - } - } - } - - var decryptedPrivateKey []byte - - if loginTwoResponse.EncryptionVersion == 1 { - log.Debug().Msg("Login version 1") - encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) - tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) - if err != nil { - util.HandleError(err) - } - - IV, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv) - if err != nil { - util.HandleError(err) - } - - paddedPassword := fmt.Sprintf("%032s", password) - key := []byte(paddedPassword) - - computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV) - if err != nil || len(computedDecryptedPrivateKey) == 0 { - util.HandleError(err) - } - - decryptedPrivateKey = computedDecryptedPrivateKey - - } else if loginTwoResponse.EncryptionVersion == 2 { - log.Debug().Msg("Login version 2") - protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey) - if err != nil { - util.HandleError(err) - } - - protectedKeyTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyTag) - if err != nil { - util.HandleError(err) - } - - protectedKeyIV, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyIV) - if err != nil { - util.HandleError(err) - } - - nonProtectedTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) - if err != nil { - util.HandleError(err) - } - - nonProtectedIv, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv) - if err != nil { - util.HandleError(err) - } - - parameters := ¶ms{ - memory: 64 * 1024, - iterations: 3, - parallelism: 1, - keyLength: 32, - } - - derivedKey, err := generateFromPassword(password, []byte(loginOneResponse.Salt), parameters) - if err != nil { - util.HandleError(fmt.Errorf("unable to generate argon hash from password [err=%s]", err)) - } - - decryptedProtectedKey, err := crypto.DecryptSymmetric(derivedKey, protectedKey, protectedKeyTag, protectedKeyIV) - if err != nil { - util.HandleError(fmt.Errorf("unable to get decrypted protected key [err=%s]", err)) - } - - encryptedPrivateKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) - if err != nil { - util.HandleError(err) - } - - decryptedProtectedKeyInHex, err := hex.DecodeString(string(decryptedProtectedKey)) - if err != nil { - util.HandleError(err) - } - - computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv) - if err != nil { - util.HandleError(err) - } - - decryptedPrivateKey = computedDecryptedPrivateKey - } else { - util.PrintErrorMessageAndExit("Insufficient details to decrypt private key") - } - - if string(decryptedPrivateKey) == "" || email == "" || loginTwoResponse.Token == "" { - log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token) - util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info") - } - // Login is successful so ask user to choose organization - newJwtToken := GetJwtTokenWithOrganizationId(loginTwoResponse.Token, email) - - //updating usercredentials - userCredentialsToBeStored.Email = email - userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey) - userCredentialsToBeStored.JTWToken = newJwtToken -} - -func init() { - rootCmd.AddCommand(loginCmd) - loginCmd.Flags().Bool("clear-domains", false, "clear all self-hosting domains from the config file") - loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line") - loginCmd.Flags().Bool("plain", false, "only output the token without any formatting") - loginCmd.Flags().String("method", "user", "login method [user, universal-auth, kubernetes, azure, gcp-id-token, gcp-iam, aws-iam, oidc-auth]") - loginCmd.Flags().String("client-id", "", "client id for universal auth") - loginCmd.Flags().String("client-secret", "", "client secret for universal auth") - loginCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods") - loginCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth") - loginCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") - loginCmd.Flags().String("jwt", "", "jwt for jwt-based auth methods [oidc-auth, jwt-auth]") - loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication. Deprecated, use --jwt instead") - - loginCmd.Flags().MarkDeprecated("oidc-jwt", "use --jwt instead") - -} - -func DomainOverridePrompt() (bool, error) { - const ( - PRESET = "Use Domain" - OVERRIDE = "Change Domain" - ) - - options := []string{PRESET, OVERRIDE} - //trim the '/' from the end of the domain url - config.INFISICAL_URL_MANUAL_OVERRIDE = strings.TrimRight(config.INFISICAL_URL_MANUAL_OVERRIDE, "/") - optionsPrompt := promptui.Select{ - Label: fmt.Sprintf("Current INFISICAL_API_URL Domain Override: %s", config.INFISICAL_URL_MANUAL_OVERRIDE), - Items: options, - Size: 2, - } - - _, selectedOption, err := optionsPrompt.Run() - if err != nil { - return false, err - } - - return selectedOption == OVERRIDE, err -} - -func usePresetDomain(presetDomain string) (bool, error) { - infisicalConfig, err := util.GetConfigFile() - if err != nil { - return false, fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err) - } - - preconfiguredUrl := strings.TrimSuffix(presetDomain, "/api") - - if preconfiguredUrl != "" && preconfiguredUrl != util.INFISICAL_DEFAULT_US_URL && preconfiguredUrl != util.INFISICAL_DEFAULT_EU_URL { - parsedDomain := strings.TrimSuffix(strings.Trim(preconfiguredUrl, "/"), "/api") - - _, err := url.ParseRequestURI(parsedDomain) - if err != nil { - return false, errors.New(fmt.Sprintf("Invalid domain URL: '%s'", parsedDomain)) - } - - config.INFISICAL_URL = fmt.Sprintf("%s/api", parsedDomain) - config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", parsedDomain) - - if !slices.Contains(infisicalConfig.Domains, parsedDomain) { - infisicalConfig.Domains = append(infisicalConfig.Domains, parsedDomain) - err = util.WriteConfigFile(&infisicalConfig) - - if err != nil { - return false, fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err) - } - } - - whilte := color.New(color.FgGreen) - boldWhite := whilte.Add(color.Bold) - time.Sleep(time.Second * 1) - boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_API_URL environment variable\n", parsedDomain) - - return true, nil - } - - return false, nil -} - -func askForDomain() error { - - // query user to choose between Infisical cloud or self-hosting - const ( - INFISICAL_CLOUD_US = "Infisical Cloud (US Region)" - INFISICAL_CLOUD_EU = "Infisical Cloud (EU Region)" - SELF_HOSTING = "Self-Hosting or Dedicated Instance" - ADD_NEW_DOMAIN = "Add a new domain" - ) - - options := []string{INFISICAL_CLOUD_US, INFISICAL_CLOUD_EU, SELF_HOSTING} - optionsPrompt := promptui.Select{ - Label: "Select your hosting option", - Items: options, - Size: 3, - } - - _, selectedHostingOption, err := optionsPrompt.Run() - if err != nil { - return err - } - - if selectedHostingOption == INFISICAL_CLOUD_US { - // US cloud option - config.INFISICAL_URL = fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) - config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", util.INFISICAL_DEFAULT_US_URL) - return nil - } else if selectedHostingOption == INFISICAL_CLOUD_EU { - // EU cloud option - config.INFISICAL_URL = fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) - config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", util.INFISICAL_DEFAULT_EU_URL) - return nil - } - - infisicalConfig, err := util.GetConfigFile() - if err != nil { - return fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err) - } - - if infisicalConfig.Domains != nil && len(infisicalConfig.Domains) > 0 { - // If domains are present in the config, let the user select from the list or select to add a new domain - - items := append(infisicalConfig.Domains, ADD_NEW_DOMAIN) - - prompt := promptui.Select{ - Label: "Which domain would you like to use?", - Items: items, - Size: 5, - } - - _, selectedOption, err := prompt.Run() - if err != nil { - return err - } - - if selectedOption != ADD_NEW_DOMAIN { - config.INFISICAL_URL = fmt.Sprintf("%s/api", selectedOption) - config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", selectedOption) - return nil - - } - - } - - urlValidation := func(input string) error { - _, err := url.ParseRequestURI(input) - if err != nil { - return errors.New("this is an invalid url") - } - return nil - } - - domainPrompt := promptui.Prompt{ - Label: "Domain", - Validate: urlValidation, - Default: "Example - https://my-self-hosted-instance.com", - } - - domain, err := domainPrompt.Run() - if err != nil { - return err - } - - // Trimmed the '/' from the end of the self-hosting url, and set the api & login url - domain = strings.TrimRight(domain, "/") - config.INFISICAL_URL = fmt.Sprintf("%s/api", domain) - config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain) - - // Write the new domain to the config file, to allow the user to select it in the future if needed - // First check if infiscialConfig.Domains already includes the domain, if it does, do not add it again - if !slices.Contains(infisicalConfig.Domains, domain) { - infisicalConfig.Domains = append(infisicalConfig.Domains, domain) - err = util.WriteConfigFile(&infisicalConfig) - - if err != nil { - return fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err) - } - } - - return nil -} - -func askForLoginCredentials() (email string, password string, err error) { - validateEmail := func(input string) error { - matched, err := regexp.MatchString("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", input) - if err != nil || !matched { - return errors.New("this doesn't look like an email address") - } - return nil - } - - fmt.Println("Enter Credentials...") - emailPrompt := promptui.Prompt{ - Label: "Email", - Validate: validateEmail, - } - - userEmail, err := emailPrompt.Run() - - if err != nil { - return "", "", err - } - - validatePassword := func(input string) error { - if len(input) < 1 { - return errors.New("please enter a valid password") - } - return nil - } - - passwordPrompt := promptui.Prompt{ - Label: "Password", - Validate: validatePassword, - Mask: '*', - } - - userPassword, err := passwordPrompt.Run() - - if err != nil { - return "", "", err - } - - return userEmail, userPassword, nil -} - -func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) { - log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password)) - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - return nil, nil, err - } - httpClient.SetRetryCount(5) - - params := srp.GetParams(4096) - secret1 := srp.GenKey() - srpClient := srp.NewClient(params, []byte(email), []byte(password), secret1) - srpA := hex.EncodeToString(srpClient.ComputeA()) - - // ** Login one - loginOneResponseResult, err := api.CallLogin1V2(httpClient, api.GetLoginOneV2Request{ - Email: email, - ClientPublicKey: srpA, - }) - - if err != nil { - return nil, nil, err - } - - // **** Login 2 - serverPublicKey_bytearray, err := hex.DecodeString(loginOneResponseResult.ServerPublicKey) - if err != nil { - return nil, nil, err - } - - userSalt, err := hex.DecodeString(loginOneResponseResult.Salt) - if err != nil { - return nil, nil, err - } - - srpClient.SetSalt(userSalt, []byte(email), []byte(password)) - srpClient.SetB(serverPublicKey_bytearray) - - srpM1 := srpClient.ComputeM1() - - loginTwoResponseResult, err := api.CallLogin2V2(httpClient, api.GetLoginTwoV2Request{ - Email: email, - ClientProof: hex.EncodeToString(srpM1), - Password: password, - }) - - if err != nil { - util.HandleError(err) - } - - return &loginOneResponseResult, &loginTwoResponseResult, nil -} - -func GetJwtTokenWithOrganizationId(oldJwtToken string, email string) string { - log.Debug().Msg(fmt.Sprint("GetJwtTokenWithOrganizationId: ", "oldJwtToken", oldJwtToken)) - - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - httpClient.SetAuthToken(oldJwtToken) - - organizationResponse, err := api.CallGetAllOrganizations(httpClient) - - if err != nil { - util.HandleError(err, "Unable to pull organizations that belong to you") - } - - organizations := organizationResponse.Organizations - - organizationNames := util.GetOrganizationsNameList(organizationResponse) - - prompt := promptui.Select{ - Label: "Which Infisical organization would you like to log into?", - Items: organizationNames, - } - - index, _, err := prompt.Run() - if err != nil { - util.HandleError(err) - } - - selectedOrganization := organizations[index] - - selectedOrgRes, err := api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrganization.ID}) - if err != nil { - util.HandleError(err) - } - - if selectedOrgRes.MfaEnabled { - i := 1 - for i < 6 { - mfaVerifyCode := askForMFACode(selectedOrgRes.MfaMethod) - - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - httpClient.SetAuthToken(selectedOrgRes.Token) - verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ - Email: email, - MFAToken: mfaVerifyCode, - MFAMethod: selectedOrgRes.MfaMethod, - }) - if requestError != nil { - util.HandleError(err) - break - } else if mfaErrorResponse != nil { - if mfaErrorResponse.Context.Code == "mfa_invalid" { - msg := fmt.Sprintf("Incorrect, verification code. You have %v attempts left", 5-i) - fmt.Println(msg) - if i == 5 { - util.PrintErrorMessageAndExit("No tries left, please try again in a bit") - break - } - } - - if mfaErrorResponse.Context.Code == "mfa_expired" { - util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again") - break - } - i++ - } else { - httpClient.SetAuthToken(verifyMFAresponse.Token) - selectedOrgRes, err = api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrganization.ID}) - break - } - } - } - - if err != nil { - util.HandleError(err, "Unable to select organization") - } - - return selectedOrgRes.Token - -} - -func userLoginMenu(currentLoggedInUserEmail string) (bool, error) { - label := fmt.Sprintf("Current logged in user email: %s on domain: %s", currentLoggedInUserEmail, config.INFISICAL_URL) - - prompt := promptui.Select{ - Label: label, - Items: []string{ADD_USER, REPLACE_USER, EXIT_USER_MENU}, - } - _, result, err := prompt.Run() - if err != nil { - return false, err - } - return result != EXIT_USER_MENU, err -} - -func generateFromPassword(password string, salt []byte, p *params) (hash []byte, err error) { - hash = argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength) - return hash, nil -} - -func askForMFACode(mfaMethod string) string { - var label string - if mfaMethod == "totp" { - label = "Enter the verification code from your mobile authenticator app or use a recovery code" - } else { - label = "Enter the 2FA verification code sent to your email" - } - mfaCodePromptUI := promptui.Prompt{ - Label: label, - } - - mfaVerifyCode, err := mfaCodePromptUI.Run() - if err != nil { - util.HandleError(err) - } - - return mfaVerifyCode -} - -func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) { - time.Sleep(time.Second * 5) - fmt.Println("\n\nOnce login is completed via browser, the CLI should be authenticated automatically.") - fmt.Println("However, if browser fails to communicate with the CLI, please paste the token from the browser below.") - - fmt.Print("\n\nToken: ") - bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - failure <- err - fmt.Println("\nError reading input:", err) - os.Exit(1) - } - - infisicalPastedToken := strings.TrimSpace(string(bytePassword)) - - userCredentials, err := decodePastedBase64Token(infisicalPastedToken) - if err != nil { - failure <- err - fmt.Println("Invalid user credentials provided", err) - os.Exit(1) - } - - // verify JTW - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - failure <- err - fmt.Println("Error getting resty client with custom headers", err) - os.Exit(1) - } - - httpClient. - SetAuthToken(userCredentials.JTWToken). - SetHeader("Accept", "application/json") - - isAuthenticated := api.CallIsAuthenticated(httpClient) - if !isAuthenticated { - fmt.Println("Invalid user credentials provided", err) - failure <- err - os.Exit(1) - } - - success <- *userCredentials -} - -func decodePastedBase64Token(token string) (*models.UserCredentials, error) { - data, err := base64.StdEncoding.DecodeString(token) - if err != nil { - return nil, err - } - var loginResponse models.UserCredentials - - err = json.Unmarshal(data, &loginResponse) - if err != nil { - return nil, err - } - - return &loginResponse, nil -} - -// Manages the browser login flow. -// Returns a UserCredentials object on success and an error on failure -func browserCliLogin() (models.UserCredentials, error) { - SERVER_TIMEOUT := 10 * 60 - - //create listener - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return models.UserCredentials{}, err - } - - //get callback port - callbackPort := listener.Addr().(*net.TCPAddr).Port - url := fmt.Sprintf("%s?callback_port=%d", config.INFISICAL_LOGIN_URL, callbackPort) - - defaultPrintStatement := fmt.Sprintf("\n\nTo complete your login, open this address in your browser: %v \n", url) - - if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { - if err := browser.OpenURL(url); err != nil { - fmt.Print(defaultPrintStatement) - } else { - fmt.Printf("\n\nPlease proceed to your browser to complete the login process.\nIf the browser doesn't open automatically, please open this address in your browser: %v \n", url) - } - } else { - fmt.Print(defaultPrintStatement) - } - - //flow channels - success := make(chan models.UserCredentials) - failure := make(chan error) - timeout := time.After(time.Second * time.Duration(SERVER_TIMEOUT)) - - //terminal state - oldState, err := term.GetState(int(os.Stdin.Fd())) - if err != nil { - return models.UserCredentials{}, err - } - - defer restoreTerminal(oldState) - - //create handler - c := cors.New(cors.Options{ - AllowedOrigins: []string{strings.ReplaceAll(config.INFISICAL_LOGIN_URL, "/login", "")}, - AllowCredentials: true, - AllowedMethods: []string{"POST", "OPTIONS"}, - AllowedHeaders: []string{"Content-Type"}, - Debug: false, - }) - corsHandler := c.Handler(browserLoginHandler(success, failure)) - - log.Debug().Msgf("Callback server listening on port %d", callbackPort) - - go http.Serve(listener, corsHandler) - go askToPasteJwtToken(success, failure) - - for { - select { - case loginResponse := <-success: - _ = closeListener(&listener) - fmt.Println("Browser login successful") - return loginResponse, nil - - case err := <-failure: - serverErr := closeListener(&listener) - return models.UserCredentials{}, errors.Join(err, serverErr) - - case <-timeout: - _ = closeListener(&listener) - return models.UserCredentials{}, errors.New("server timeout") - } - } -} - -func restoreTerminal(oldState *term.State) { - term.Restore(int(os.Stdin.Fd()), oldState) -} - -// // listens to 'q' input on terminal and -// // sends 'true' to 'quit' channel -// func quitBrowserLogin(quit chan bool, oState *term.State) { -// oldState, err := term.MakeRaw(int(os.Stdin.Fd())) -// if err != nil { -// return -// } -// *oState = *oldState -// defer restoreTerminal(oldState) -// b := make([]byte, 1) -// for { -// _, _ = os.Stdin.Read(b) -// if string(b) == QUIT_BROWSER_LOGIN { -// quit <- true -// break -// } -// } -// } - -func closeListener(listener *net.Listener) error { - err := (*listener).Close() - if err != nil { - return err - } - log.Debug().Msg("Callback server shutdown successfully") - return nil -} - -func browserLoginHandler(success chan models.UserCredentials, failure chan error) http.HandlerFunc { - - return func(w http.ResponseWriter, r *http.Request) { - var loginResponse models.UserCredentials - - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&loginResponse) - if err != nil { - failure <- err - } - - w.WriteHeader(http.StatusOK) - success <- loginResponse - - } -} diff --git a/cli/packages/cmd/man.go b/cli/packages/cmd/man.go deleted file mode 100644 index 86d68c8e7..000000000 --- a/cli/packages/cmd/man.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "fmt" - "os" - - mcobra "github.com/muesli/mango-cobra" - "github.com/muesli/roff" - "github.com/spf13/cobra" -) - -var manCmd = &cobra.Command{ - Use: "man", - Short: "generates the manpages", - SilenceUsage: true, - DisableFlagsInUseLine: true, - Hidden: true, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - manPage, err := mcobra.NewManPage(1, rootCmd) - if err != nil { - return err - } - - _, err = fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument())) - return err - }, -} - -func init() { - rootCmd.AddCommand(manCmd) -} diff --git a/cli/packages/cmd/pre-commit-script/pre-commit-without-bang.sh b/cli/packages/cmd/pre-commit-script/pre-commit-without-bang.sh deleted file mode 100644 index e47643cc9..000000000 --- a/cli/packages/cmd/pre-commit-script/pre-commit-without-bang.sh +++ /dev/null @@ -1,20 +0,0 @@ - - -# MANAGED BY INFISICAL CLI (Do not modify): START -infisicalScanEnabled=$(git config --bool hooks.infisical-scan) - -if [ "$infisicalScanEnabled" != "false" ]; then - infisical scan git-changes -v --staged - exitCode=$? - if [ $exitCode -eq 1 ]; then - echo "Commit blocked: Infisical scan has uncovered secrets in your git commit" - echo "To disable the Infisical scan precommit hook run the following command:" - echo "" - echo " git config hooks.infisical-scan false" - echo "" - exit 1 - fi -else - echo 'Warning: infisical scan precommit disabled' -fi -# MANAGED BY INFISICAL CLI (Do not modify): END \ No newline at end of file diff --git a/cli/packages/cmd/pre-commit-script/pre-commit.sh b/cli/packages/cmd/pre-commit-script/pre-commit.sh deleted file mode 100644 index f899a1a51..000000000 --- a/cli/packages/cmd/pre-commit-script/pre-commit.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -# MANAGED BY INFISICAL CLI (Do not modify): START -infisicalScanEnabled=$(git config --bool hooks.infisical-scan) - -if [ "$infisicalScanEnabled" != "false" ]; then - infisical scan git-changes -v --staged - exitCode=$? - if [ $exitCode -eq 1 ]; then - echo "Commit blocked: Infisical scan has uncovered secrets in your git commit" - echo "To disable the Infisical scan precommit hook run the following command:" - echo "" - echo " git config hooks.infisical-scan false" - echo "" - exit 1 - fi -else - echo 'Warning: infisical scan precommit disabled' -fi -# MANAGED BY INFISICAL CLI (Do not modify): END \ No newline at end of file diff --git a/cli/packages/cmd/reset.go b/cli/packages/cmd/reset.go deleted file mode 100644 index dcc8cebe4..000000000 --- a/cli/packages/cmd/reset.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "os" - - "github.com/Infisical/infisical-merge/packages/util" - "github.com/posthog/posthog-go" - "github.com/spf13/cobra" -) - -var resetCmd = &cobra.Command{ - Use: "reset", - Short: "Used to delete all Infisical related data on your machine", - DisableFlagsInUseLine: true, - Example: "infisical reset", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - // delete keyring item of current logged in user - configFile, _ := util.GetConfigFile() - - // delete from keyring - util.DeleteValueInKeyring(configFile.LoggedInUserEmail) - - // delete config - _, pathToDir, err := util.GetFullConfigFilePath() - if err != nil { - util.HandleError(err) - } - - os.RemoveAll(pathToDir) - - // delete secrets backup - util.DeleteBackupSecrets() - - util.PrintSuccessMessage("Reset successful") - Telemetry.CaptureEvent("cli-command:reset", posthog.NewProperties().Set("version", util.CLI_VERSION)) - }, -} - -func init() { - rootCmd.AddCommand(resetCmd) -} diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go deleted file mode 100644 index b9370ad89..000000000 --- a/cli/packages/cmd/root.go +++ /dev/null @@ -1,104 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "fmt" - "os" - "strings" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/telemetry" - "github.com/Infisical/infisical-merge/packages/util" -) - -var Telemetry *telemetry.Telemetry - -var rootCmd = &cobra.Command{ - Use: "infisical", - Short: "Infisical CLI is used to inject environment variables into any process", - Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`, - CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true}, - Version: util.CLI_VERSION, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(initLog) - rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)") - rootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary") - rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL), "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]") - rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.") - rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - silent, err := cmd.Flags().GetBool("silent") - if err != nil { - util.HandleError(err) - } - - config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL) - - // util.DisplayAptInstallationChangeBanner(silent) - if !util.IsRunningInDocker() && !silent { - util.CheckForUpdate() - } - - loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false) - - if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired { - token, err := util.GetInfisicalToken(cmd) - - if err == nil && token != nil { - util.PrintWarning(fmt.Sprintf("Your logged-in session is being overwritten by the token provided from the %s.", token.Source)) - } - } - - } - - // if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment - // this is used to allow overrides of the default value - if !rootCmd.Flag("domain").Changed { - if envInfisicalBackendUrl, ok := os.LookupEnv("INFISICAL_API_URL"); ok { - config.INFISICAL_URL = envInfisicalBackendUrl - } - } - - isTelemetryOn, _ := rootCmd.PersistentFlags().GetBool("telemetry") - Telemetry = telemetry.NewTelemetry(isTelemetryOn) -} - -func initLog() { - zerolog.SetGlobalLevel(zerolog.InfoLevel) - ll, err := rootCmd.Flags().GetString("log-level") - if err != nil { - log.Fatal().Msg(err.Error()) - } - switch strings.ToLower(ll) { - case "trace": - zerolog.SetGlobalLevel(zerolog.TraceLevel) - case "debug": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "info": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "warn": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - case "err", "error": - zerolog.SetGlobalLevel(zerolog.ErrorLevel) - case "fatal": - zerolog.SetGlobalLevel(zerolog.FatalLevel) - default: - zerolog.SetGlobalLevel(zerolog.InfoLevel) - } -} diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go deleted file mode 100644 index 7f11a3f95..000000000 --- a/cli/packages/cmd/run.go +++ /dev/null @@ -1,491 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "errors" - "fmt" - "os" - "os/exec" - "os/signal" - "runtime" - "strings" - "sync" - "syscall" - "time" - - "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/fatih/color" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -var ErrManualSignalInterrupt = errors.New("signal: interrupt") -var watcherWaitGroup = new(sync.WaitGroup) - -// runCmd represents the run command -var runCmd = &cobra.Command{ - Example: ` - infisical run --env=dev -- npm run dev - infisical run --command "first-command && second-command; more-commands..." - `, - Use: "run [any infisical run command flags] -- [your application start command]", - Short: "Used to inject environments variables into your application process", - DisableFlagsInUseLine: true, - Args: func(cmd *cobra.Command, args []string) error { - // Check if the --command flag has been set - commandFlagSet := cmd.Flags().Changed("command") - - // If the --command flag has been set, check if a value was provided - if commandFlagSet { - command := cmd.Flag("command").Value.String() - if command == "" { - return fmt.Errorf("you need to provide a command after the flag --command") - } - - // If the --command flag has been set, args should not be provided - if len(args) > 0 { - return fmt.Errorf("you cannot set any arguments after --command flag. --command only takes a string command") - } - } else { - // If the --command flag has not been set, at least one arg should be provided - if len(args) == 0 { - return fmt.Errorf("at least one argument is required after the run command, received %d", len(args)) - } - } - - return nil - }, - Run: func(cmd *cobra.Command, args []string) { - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectConfigDir, err := cmd.Flags().GetString("project-config-dir") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - command, err := cmd.Flags().GetString("command") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretOverriding, err := cmd.Flags().GetBool("secret-overriding") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - watchMode, err := cmd.Flags().GetBool("watch") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - watchModeInterval, err := cmd.Flags().GetInt("watch-interval") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - // If the --watch flag has been set, the --watch-interval flag should also be set - if watchMode && watchModeInterval < 5 { - util.HandleError(fmt.Errorf("watch interval must be at least 5 seconds, you passed %d seconds", watchModeInterval)) - } - - shouldExpandSecrets, err := cmd.Flags().GetBool("expand") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - tagSlugs, err := cmd.Flags().GetString("tags") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - includeImports, err := cmd.Flags().GetBool("include-imports") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - recursive, err := cmd.Flags().GetBool("recursive") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - request := models.GetAllSecretsParameters{ - Environment: environmentName, - WorkspaceId: projectId, - TagSlugs: tagSlugs, - SecretsPath: secretsPath, - IncludeImport: includeImports, - Recursive: recursive, - ExpandSecretReferences: shouldExpandSecrets, - } - - injectableEnvironment, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token) - if err != nil { - util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") - } - - log.Debug().Msgf("injecting the following environment variables into shell: %v", injectableEnvironment.Variables) - - if watchMode { - executeCommandWithWatchMode(command, args, watchModeInterval, request, projectConfigDir, secretOverriding, token) - } else { - if cmd.Flags().Changed("command") { - command := cmd.Flag("command").Value.String() - err = executeMultipleCommandWithEnvs(command, injectableEnvironment.SecretsCount, injectableEnvironment.Variables) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - } else { - err = executeSingleCommandWithEnvs(args, injectableEnvironment.SecretsCount, injectableEnvironment.Variables) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - } - } - - }, -} - -func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) { - var ( - reservedEnvVars = []string{ - "HOME", "PATH", "PS1", "PS2", - "PWD", "EDITOR", "XAUTHORITY", "USER", - "TERM", "TERMINFO", "SHELL", "MAIL", - } - - reservedEnvVarPrefixes = []string{ - "XDG_", - "LC_", - } - ) - - for _, reservedEnvName := range reservedEnvVars { - if _, ok := env[reservedEnvName]; ok { - delete(env, reservedEnvName) - util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it is a reserved secret name", reservedEnvName)) - } - } - - for _, reservedEnvPrefix := range reservedEnvVarPrefixes { - for envName := range env { - if strings.HasPrefix(envName, reservedEnvPrefix) { - delete(env, envName) - util.PrintWarning(fmt.Sprintf("Infisical secret named [%v] has been removed because it contains a reserved prefix", envName)) - } - } - } -} - -func init() { - rootCmd.AddCommand(runCmd) - runCmd.Flags().String("token", "", "fetch secrets using service token or machine identity access token") - runCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") - runCmd.Flags().StringP("env", "e", "dev", "set the environment (dev, prod, etc.) from which your secrets should be pulled from") - runCmd.Flags().Bool("expand", true, "parse shell parameter expansions in your secrets") - runCmd.Flags().Bool("include-imports", true, "import linked secrets ") - runCmd.Flags().Bool("recursive", false, "fetch secrets from all sub-folders") - runCmd.Flags().Bool("secret-overriding", true, "prioritizes personal secrets, if any, with the same name over shared secrets") - runCmd.Flags().Bool("watch", false, "enable reload of application when secrets change") - runCmd.Flags().Int("watch-interval", 10, "interval in seconds to check for secret changes") - runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") - runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") - runCmd.Flags().String("path", "/", "get secrets within a folder path") - runCmd.Flags().String("project-config-dir", "", "explicitly set the directory where the .infisical.json resides") -} - -// Will execute a single command and pass in the given secrets into the process -func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) error { - command := args[0] - argsForCommand := args[1:] - - log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount)) - - cmd := exec.Command(command, argsForCommand...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Env = env - - return execBasicCmd(cmd) -} - -func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error { - shell := [2]string{"sh", "-c"} - if runtime.GOOS == "windows" { - shell = [2]string{"cmd", "/C"} - } else { - currentShell := os.Getenv("SHELL") - if currentShell != "" { - shell[0] = currentShell - } - } - - cmd := exec.Command(shell[0], shell[1], fullCommand) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Env = env - - log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount)) - log.Debug().Msgf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand) - - return execBasicCmd(cmd) -} - -func execBasicCmd(cmd *exec.Cmd) error { - sigChannel := make(chan os.Signal, 1) - signal.Notify(sigChannel) - - if err := cmd.Start(); err != nil { - return err - } - - go func() { - for { - sig := <-sigChannel - _ = cmd.Process.Signal(sig) // process all sigs - } - }() - - if err := cmd.Wait(); err != nil { - _ = cmd.Process.Signal(os.Kill) - return fmt.Errorf("failed to wait for command termination: %v", err) - } - - waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) - os.Exit(waitStatus.ExitStatus()) - return nil -} - -func waitForExitCommand(cmd *exec.Cmd) (int, error) { - if err := cmd.Wait(); err != nil { - // ignore errors - cmd.Process.Signal(os.Kill) // #nosec G104 - - if exitError, ok := err.(*exec.ExitError); ok { - return exitError.ExitCode(), exitError - } - - return 2, err - } - - waitStatus, ok := cmd.ProcessState.Sys().(syscall.WaitStatus) - if !ok { - return 2, fmt.Errorf("unexpected ProcessState type, expected syscall.WaitStatus, got %T", waitStatus) - } - return waitStatus.ExitStatus(), nil -} - -func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) { - - var cmd *exec.Cmd - var err error - var lastSecretsFetch time.Time - var lastUpdateEvent time.Time - var watchMutex sync.Mutex - var processMutex sync.Mutex - var beingTerminated = false - var currentETag string - - if err != nil { - util.HandleError(err, "Failed to fetch secrets") - } - - runCommandWithWatcher := func(environmentVariables models.InjectableEnvironmentResult) { - currentETag = environmentVariables.ETag - secretsFetchedAt := time.Now() - if secretsFetchedAt.After(lastSecretsFetch) { - lastSecretsFetch = secretsFetchedAt - } - - shouldRestartProcess := cmd != nil - // terminate the old process before starting a new one - if shouldRestartProcess { - log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Environment changes detected. Reloading process...")) - beingTerminated = true - - log.Debug().Msgf(color.HiMagentaString("[HOT RELOAD] Sending SIGTERM to PID %d", cmd.Process.Pid)) - if e := cmd.Process.Signal(syscall.SIGTERM); e != nil { - log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGTERM")) - } - // wait up to 10 sec for the process to exit - for i := 0; i < 10; i++ { - if !util.IsProcessRunning(cmd.Process) { - // process has been killed so we break out - break - } - if i == 5 { - log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Still waiting for process exit status")) - } - time.Sleep(time.Second) - } - - // SIGTERM may not work on Windows so we try SIGKILL - if util.IsProcessRunning(cmd.Process) { - log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Process still hasn't fully exited, attempting SIGKILL")) - if e := cmd.Process.Kill(); e != nil { - log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGKILL")) - } - } - - cmd = nil - } else { - // If `cmd` is nil, we know this is the first time we are starting the process - log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Watching for secret changes...")) - } - - processMutex.Lock() - - if lastUpdateEvent.After(secretsFetchedAt) { - processMutex.Unlock() - return - } - - beingTerminated = false - watcherWaitGroup.Add(1) - - // start the process - log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", environmentVariables.SecretsCount)) - - cmd, err = util.RunCommand(commandFlag, args, environmentVariables.Variables, false) - if err != nil { - defer watcherWaitGroup.Done() - util.HandleError(err) - } - - go func() { - defer processMutex.Unlock() - defer watcherWaitGroup.Done() - - exitCode, err := waitForExitCommand(cmd) - - // ignore errors if we are being terminated - if !beingTerminated { - if err != nil { - if strings.HasPrefix(err.Error(), "exec") || strings.HasPrefix(err.Error(), "fork/exec") { - log.Error().Err(err).Msg("Failed to execute command") - } - if err.Error() != ErrManualSignalInterrupt.Error() { - log.Error().Err(err).Msg("Process exited with error") - } - } - - os.Exit(exitCode) - } - }() - } - - recheckSecretsChannel := make(chan bool, 1) - recheckSecretsChannel <- true - - // a simple goroutine that triggers the recheckSecretsChan every watch interval (defaults to 10 seconds) - go func() { - for { - time.Sleep(time.Duration(watchModeInterval) * time.Second) - recheckSecretsChannel <- true - } - }() - - for { - <-recheckSecretsChannel - func() { - watchMutex.Lock() - defer watchMutex.Unlock() - - newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token) - if err != nil { - log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") - return - } - - if newEnvironmentVariables.ETag != currentETag { - runCommandWithWatcher(newEnvironmentVariables) - } else { - log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") - } - - }() - } -} - -func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) { - - if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { - request.InfisicalToken = token.Token - } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - request.UniversalAuthAccessToken = token.Token - } - - secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir) - - if err != nil { - return models.InjectableEnvironmentResult{}, err - } - - if secretOverriding { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL) - } else { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED) - } - - secretsByKey := getSecretsByKeys(secrets) - environmentVariables := make(map[string]string) - - // add all existing environment vars - for _, s := range os.Environ() { - kv := strings.SplitN(s, "=", 2) - key := kv[0] - value := kv[1] - environmentVariables[key] = value - } - - // check to see if there are any reserved key words in secrets to inject - filterReservedEnvVars(secretsByKey) - - // now add infisical secrets - for k, v := range secretsByKey { - environmentVariables[k] = v.Value - } - - env := make([]string, 0, len(environmentVariables)) - for key, value := range environmentVariables { - env = append(env, key+"="+value) - } - - return models.InjectableEnvironmentResult{ - Variables: env, - ETag: util.GenerateETagFromSecrets(secrets), - SecretsCount: len(secretsByKey), - }, nil -} diff --git a/cli/packages/cmd/scan.go b/cli/packages/cmd/scan.go deleted file mode 100644 index 4a721d2c5..000000000 --- a/cli/packages/cmd/scan.go +++ /dev/null @@ -1,636 +0,0 @@ -// MIT License - -// Copyright (c) 2019 Zachary Rice - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package cmd - -import ( - _ "embed" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/Infisical/infisical-merge/detect" - "github.com/Infisical/infisical-merge/detect/cmd/scm" - "github.com/Infisical/infisical-merge/detect/config" - "github.com/Infisical/infisical-merge/detect/logging" - "github.com/Infisical/infisical-merge/detect/report" - "github.com/Infisical/infisical-merge/detect/sources" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/manifoldco/promptui" - "github.com/posthog/posthog-go" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -const configDescription = `config file path -order of precedence: -1. --config flag -2. env var INFISICAL_SCAN_CONFIG -3. (--source/-s)/.infisical-scan.toml -If none of the three options are used, then Infisical will use the default scan config` - -//go:embed pre-commit-script/pre-commit.sh -var preCommitTemplate []byte - -//go:embed pre-commit-script/pre-commit-without-bang.sh -var preCommitTemplateAppend []byte - -const ( - defaultHooksPath = ".git/hooks/" - preCommitFile = "pre-commit" -) - -func init() { - // scan flag for only scan command - scanCmd.Flags().String("log-opts", "", "git log options") - scanCmd.Flags().Bool("no-git", false, "treat git repo as a regular directory and scan those files, --log-opts has no effect on the scan when --no-git is set") - scanCmd.Flags().Bool("pipe", false, "scan input from stdin, ex: `cat some_file | infisical scan --pipe`") - scanCmd.Flags().Bool("follow-symlinks", false, "scan files that are symlinks to other files") - - // global scan flags - scanCmd.PersistentFlags().StringP("config", "c", "", configDescription) - scanCmd.PersistentFlags().Int("exit-code", 1, "exit code when leaks have been encountered") - scanCmd.PersistentFlags().StringP("source", "s", ".", "path to source") - scanCmd.PersistentFlags().StringP("report-path", "r", "", "report file") - scanCmd.PersistentFlags().StringP("report-format", "f", "json", "output format (json, csv, sarif)") - scanCmd.PersistentFlags().StringP("baseline-path", "b", "", "path to baseline with issues that can be ignored") - scanCmd.PersistentFlags().BoolP("verbose", "v", false, "show verbose output from scan (which file, where in the file, what secret)") - scanCmd.PersistentFlags().BoolP("no-color", "", false, "turn off color for verbose output") - scanCmd.PersistentFlags().Int("max-target-megabytes", 0, "files larger than this will be skipped") - scanCmd.PersistentFlags().Bool("redact", false, "redact secrets from logs and stdout") - - // scan git changes command flags - scanGitChangesCmd.Flags().Bool("staged", false, "detect secrets in a --staged state") - scanGitChangesCmd.Flags().String("log-opts", "", "git log options") - - // find config source - err := viper.BindPFlag("config", scanCmd.PersistentFlags().Lookup("config")) - if err != nil { - log.Fatal().Msgf("err binding config %s", err.Error()) - } - - // add flags to main - scanCmd.AddCommand(scanGitChangesCmd) - rootCmd.AddCommand(scanCmd) - - installCmd.Flags().Bool("pre-commit-hook", false, "installs pre commit hook for Git repository") - scanCmd.AddCommand(installCmd) -} - -func initScanConfig(cmd *cobra.Command) { - cfgPath, err := cmd.Flags().GetString("config") - if err != nil { - log.Fatal().Msg(err.Error()) - } - - if cfgPath != "" { - viper.SetConfigFile(cfgPath) - log.Debug().Msgf("using scan config %s from `--config`", cfgPath) - } else if os.Getenv(config.DefaultScanConfigEnvName) != "" { - envPath := os.Getenv(config.DefaultScanConfigEnvName) - viper.SetConfigFile(envPath) - log.Debug().Msgf("using scan config from %s env var: %s", config.DefaultScanConfigEnvName, envPath) - } else { - source, err := cmd.Flags().GetString("source") - if err != nil { - log.Fatal().Msg(err.Error()) - } - fileInfo, err := os.Stat(source) - if err != nil { - log.Fatal().Msg(err.Error()) - } - - if !fileInfo.IsDir() { - log.Debug().Msgf("unable to load scan config from %s since --source=%s is a file, using default config", - filepath.Join(source, config.DefaultScanConfigFileName), source) - viper.SetConfigType("toml") - if err = viper.ReadConfig(strings.NewReader(config.DefaultConfig)); err != nil { - log.Fatal().Msgf("err reading toml %s", err.Error()) - } - return - } - - if _, err := os.Stat(filepath.Join(source, config.DefaultScanConfigFileName)); os.IsNotExist(err) { - log.Debug().Msgf("no scan config found in path %s, using default scan config", filepath.Join(source, config.DefaultScanConfigFileName)) - viper.SetConfigType("toml") - if err = viper.ReadConfig(strings.NewReader(config.DefaultConfig)); err != nil { - log.Fatal().Msgf("err reading default scan config toml %s", err.Error()) - } - return - } else { - log.Debug().Msgf("using existing scan config %s from `(--source)/%s`", filepath.Join(source, config.DefaultScanConfigFileName), config.DefaultScanConfigFileName) - } - - viper.AddConfigPath(source) - viper.SetConfigName(config.DefaultScanConfigFileName) - viper.SetConfigType("toml") - } - if err := viper.ReadInConfig(); err != nil { - log.Fatal().Msgf("unable to load scan config, err: %s", err) - } -} - -var installCmd = &cobra.Command{ - Use: "install", - Short: "Install scanning scripts and tools. Use --help flag to see all options", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - installPrecommit := cmd.Flags().Changed("pre-commit-hook") - if installPrecommit { - hooksPath, err := getHooksPath() - if err != nil { - fmt.Printf("Error: %s\n", err) - return - } - - if hooksPath != ".git/hooks" { - defaultHookOverride, err := overrideDefaultHooksPath(hooksPath) - if err != nil { - fmt.Printf("Error: %s\n", err) - } - - if defaultHookOverride { - ConfigureGitHooksPath() - - log.Info().Msgf("To switch back previous githooks manager run: git config core.hooksPath %s\n", hooksPath) - return - } else { - log.Warn().Msgf("To automatically configure this hook, you need to switch the path of the Hooks. Alternatively, you can manually configure this hook by setting your pre-commit script to run command [infisical scan git-changes -v --staged].\n") - return - } - } - - err = createOrUpdatePreCommitFile(hooksPath) - if err != nil { - fmt.Printf("Error: %s\n", err) - return - } - - log.Info().Msgf("Pre-commit hook successfully added. Infisical scan should now run on each commit you make\n") - - Telemetry.CaptureEvent("cli-command:install --pre-commit-hook", posthog.NewProperties().Set("version", util.CLI_VERSION)) - - return - } - }} - -var scanCmd = &cobra.Command{ - Use: "scan", - Short: "Scan for leaked secrets in git history, directories, and files", - Run: func(cmd *cobra.Command, args []string) { - initScanConfig(cmd) - - var ( - vc config.ViperConfig - findings []report.Finding - err error - ) - - // Load config - if err = viper.Unmarshal(&vc); err != nil { - log.Fatal().Err(err).Msg("Failed to load config") - } - cfg, err := vc.Translate() - if err != nil { - log.Fatal().Err(err).Msg("Failed to load config") - } - cfg.Path, _ = cmd.Flags().GetString("config") - - // start timer - start := time.Now() - - // Setup detector - detector := detect.NewDetector(cfg) - detector.Config.Path, err = cmd.Flags().GetString("config") - if err != nil { - log.Fatal().Err(err).Msg("") - } - source, err := cmd.Flags().GetString("source") - if err != nil { - log.Fatal().Err(err).Msg("") - } - // if config path is not set, then use the {source}/.infisical-scan.toml path. - // note that there may not be a `{source}/.infisical-scan.toml` file, this is ok. - if detector.Config.Path == "" { - detector.Config.Path = filepath.Join(source, config.DefaultScanConfigFileName) - } - // set verbose flag - if detector.Verbose, err = cmd.Flags().GetBool("verbose"); err != nil { - log.Fatal().Err(err).Msg("") - } - // set redact flag - - redactFlag, err := cmd.Flags().GetBool("redact") - if err != nil { - log.Fatal().Err(err).Msg("") - } - if redactFlag { - detector.Redact = 100 - } else { - detector.Redact = 0 - } - - if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil { - log.Fatal().Err(err).Msg("") - } - // set color flag - if detector.NoColor, err = cmd.Flags().GetBool("no-color"); err != nil { - log.Fatal().Err(err).Msg("") - } - - if fileExists(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)) { - if err = detector.AddGitleaksIgnore(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)); err != nil { - log.Fatal().Err(err).Msg("could not call AddInfisicalIgnore") - } - } - - // ignore findings from the baseline (an existing report in json format generated earlier) - baselinePath, _ := cmd.Flags().GetString("baseline-path") - if baselinePath != "" { - err = detector.AddBaseline(baselinePath, source) - if err != nil { - log.Error().Msgf("Could not load baseline. The path must point to report generated by `infisical scan` using the default format: %s", err) - } - } - - // set follow symlinks flag - if detector.FollowSymlinks, err = cmd.Flags().GetBool("follow-symlinks"); err != nil { - log.Fatal().Err(err).Msg("") - } - - // set exit code - exitCode, err := cmd.Flags().GetInt("exit-code") - if err != nil { - log.Fatal().Err(err).Msg("could not get exit code") - } - - // determine what type of scan: - // - git: scan the history of the repo - // - no-git: scan files by treating the repo as a plain directory - noGit, err := cmd.Flags().GetBool("no-git") - if err != nil { - log.Fatal().Err(err).Msg("could not call GetBool() for no-git") - } - fromPipe, err := cmd.Flags().GetBool("pipe") - if err != nil { - log.Fatal().Err(err) - } - - log.Info().Msgf("scanning for exposed secrets...") - - // start the detector scan - if noGit { - paths, err := sources.DirectoryTargets( - source, - detector.Sema, - detector.FollowSymlinks, - detector.Config.Allowlists, - ) - if err != nil { - logging.Fatal().Err(err).Send() - } - - if findings, err = detector.DetectFiles(paths); err != nil { - // don't exit on error, just log it - logging.Error().Err(err).Msg("failed scan directory") - } - } else if fromPipe { - if findings, err = detector.DetectReader(os.Stdin, 10); err != nil { - // log fatal to exit, no need to continue since a report - // will not be generated when scanning from a pipe...for now - logging.Fatal().Err(err).Msg("failed scan input from stdin") - } - } else { - var ( - gitCmd *sources.GitCmd - scmPlatform scm.Platform - remote *detect.RemoteInfo - ) - - var logOpts string - logOpts, err = cmd.Flags().GetString("log-opts") - - if gitCmd, err = sources.NewGitLogCmd(source, logOpts); err != nil { - logging.Fatal().Err(err).Msg("could not create Git cmd") - } - scmPlatform = scm.UnknownPlatform - remote = detect.NewRemoteInfo(scmPlatform, source) - - if findings, err = detector.DetectGit(gitCmd, remote); err != nil { - // don't exit on error, just log it - logging.Error().Err(err).Msg("failed to scan Git repository") - } - } - // log info about the scan - if err == nil { - log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start))) - if len(findings) != 0 { - log.Warn().Msgf("leaks found: %d", len(findings)) - } else { - log.Info().Msg("no leaks found") - } - } else { - log.Warn().Msgf("partial scan completed in %s", FormatDuration(time.Since(start))) - if len(findings) != 0 { - log.Warn().Msgf("%d leaks found in partial scan", len(findings)) - } else { - log.Warn().Msg("no leaks found in partial scan") - } - } - - Telemetry.CaptureEvent("cli-command:scan", posthog.NewProperties().Set("risks", len(findings)).Set("version", util.CLI_VERSION)) - - // write report if desired - reportPath, _ := cmd.Flags().GetString("report-path") - ext, _ := cmd.Flags().GetString("report-format") - if reportPath != "" { - reportFindings(findings, reportPath, ext, &cfg) - } - - if err != nil { - os.Exit(1) - } - - if len(findings) != 0 { - os.Exit(exitCode) - } - }, -} - -var scanGitChangesCmd = &cobra.Command{ - Use: "git-changes", - Short: "Scan for secrets in uncommitted changes in a git repo", - Run: func(cmd *cobra.Command, args []string) { - initScanConfig(cmd) - - var vc config.ViperConfig - - if err := viper.Unmarshal(&vc); err != nil { - log.Fatal().Err(err).Msg("Failed to load config") - } - cfg, err := vc.Translate() - if err != nil { - log.Fatal().Err(err).Msg("Failed to load config") - } - - cfg.Path, _ = cmd.Flags().GetString("config") - exitCode, _ := cmd.Flags().GetInt("exit-code") - staged, _ := cmd.Flags().GetBool("staged") - - // Setup detector - detector := detect.NewDetector(cfg) - detector.Config.Path, err = cmd.Flags().GetString("config") - if err != nil { - log.Fatal().Err(err).Msg("") - } - source, err := cmd.Flags().GetString("source") - if err != nil { - log.Fatal().Err(err).Msg("") - } - // if config path is not set, then use the {source}/.infisical-scan.toml path. - // note that there may not be a `{source}/.infisical-scan.toml` file, this is ok. - if detector.Config.Path == "" { - detector.Config.Path = filepath.Join(source, config.DefaultScanConfigFileName) - } - // set verbose flag - if detector.Verbose, err = cmd.Flags().GetBool("verbose"); err != nil { - log.Fatal().Err(err).Msg("") - } - // set redact flag - - redactFlag, err := cmd.Flags().GetBool("redact") - if err != nil { - log.Fatal().Err(err).Msg("") - } - if redactFlag { - detector.Redact = 100 - } else { - detector.Redact = 0 - } - - if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil { - log.Fatal().Err(err).Msg("") - } - // set color flag - if detector.NoColor, err = cmd.Flags().GetBool("no-color"); err != nil { - log.Fatal().Err(err).Msg("") - } - - if fileExists(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)) { - if err = detector.AddGitleaksIgnore(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)); err != nil { - log.Fatal().Err(err).Msg("could not call AddInfisicalIgnore") - } - } - - // start git scan - var ( - findings []report.Finding - - gitCmd *sources.GitCmd - remote *detect.RemoteInfo - ) - - if gitCmd, err = sources.NewGitDiffCmd(source, staged); err != nil { - logging.Fatal().Err(err).Msg("could not create Git diff cmd") - } - remote = &detect.RemoteInfo{Platform: scm.NoPlatform} - - if findings, err = detector.DetectGit(gitCmd, remote); err != nil { - // don't exit on error, just log it - logging.Error().Err(err).Msg("failed to scan Git repository") - } - - Telemetry.CaptureEvent("cli-command:scan git-changes", posthog.NewProperties().Set("risks", len(findings)).Set("version", util.CLI_VERSION)) - - reportPath, _ := cmd.Flags().GetString("report-path") - ext, _ := cmd.Flags().GetString("report-format") - if reportPath != "" { - reportFindings(findings, reportPath, ext, &cfg) - } - if len(findings) != 0 { - os.Exit(exitCode) - } - }, -} - -func reportFindings(findings []report.Finding, reportPath string, ext string, cfg *config.Config) { - - var reporter report.Reporter - - switch ext { - case "csv": - reporter = &report.CsvReporter{} - case "json": - reporter = &report.JsonReporter{} - case "junit": - reporter = &report.JunitReporter{} - case "sarif": - reporter = &report.SarifReporter{ - OrderedRules: cfg.GetOrderedRules(), - } - default: - logging.Fatal().Msgf("unknown report format %s", ext) - } - - file, err := os.Create(reportPath) - if err != nil { - log.Fatal().Err(err).Msg("could not create file") - } - - if err := reporter.Write(file, findings); err != nil { - log.Fatal().Err(err).Msg("could not write") - } - -} - -func fileExists(fileName string) bool { - // check for a .infisicalignore file - info, err := os.Stat(fileName) - if err != nil && !os.IsNotExist(err) { - return false - } - - if info != nil && err == nil { - if !info.IsDir() { - return true - } - } - return false -} - -func FormatDuration(d time.Duration) string { - scale := 100 * time.Second - // look for the max scale that is smaller than d - for scale > d { - scale = scale / 10 - } - return d.Round(scale / 100).String() -} - -func overrideDefaultHooksPath(managedHook string) (bool, error) { - YES := "Yes" - NO := "No" - - options := []string{YES, NO} - optionsPrompt := promptui.Select{ - Label: fmt.Sprintf("Your hooks path is set to [%s] but needs to be [.git/hooks] for automatic configuration. Would you like to switch? ", managedHook), - Items: options, - Size: 2, - } - - _, selectedOption, err := optionsPrompt.Run() - if err != nil { - return false, err - } - - return selectedOption == YES, err -} - -func ConfigureGitHooksPath() { - cmd := exec.Command("git", "config", "core.hooksPath", ".git/hooks") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - log.Fatal().Msgf("Failed to configure git hooks path: %v", err) - } -} - -// GetGitRoot returns the root directory of the current Git repository. -func GetGitRoot() (string, error) { - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - output, err := cmd.Output() - - if err != nil { - return "", fmt.Errorf("failed to get git root directory: %w", err) - } - - gitRoot := strings.TrimSpace(string(output)) // Remove any trailing newline - return gitRoot, nil -} - -func getHooksPath() (string, error) { - out, err := exec.Command("git", "config", "core.hooksPath").Output() - if err != nil { - if len(out) == 0 { - out = []byte(".git/hooks") // set the default hook - } else { - log.Error().Msgf("Failed to get Git hooks path: %s\nOutput: %s\n", err, out) - } - } - - hooksPath := strings.TrimSpace(string(out)) - return hooksPath, nil -} - -func createOrUpdatePreCommitFile(hooksPath string) error { - // File doesn't exist, create a new one - rootGitRepoPath, err := GetGitRoot() - if err != nil { - return err - } - - filePath := fmt.Sprintf("%s/%s/%s", rootGitRepoPath, hooksPath, preCommitFile) - - _, err = os.Stat(filePath) - if err == nil { - // File already exists, check if it contains the managed comments - content, err := ioutil.ReadFile(filePath) - if err != nil { - return fmt.Errorf("failed to read pre-commit file: %s", err) - } - - if strings.Contains(string(content), "# MANAGED BY INFISICAL CLI (Do not modify): START") && - strings.Contains(string(content), "# MANAGED BY INFISICAL CLI (Do not modify): END") { - return nil - } - - // File already exists, append the template content - file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0755) - if err != nil { - return fmt.Errorf("failed to open pre-commit file: %s", err) - } - - defer file.Close() - - _, err = file.Write(preCommitTemplateAppend) - if err != nil { - return fmt.Errorf("failed to append to pre-commit file: %s", err) - } - - } else if os.IsNotExist(err) { - err = os.WriteFile(filePath, preCommitTemplate, 0755) - if err != nil { - return fmt.Errorf("failed to create pre-commit file: %s", err) - } - } else { - // Error occurred while checking file status - return fmt.Errorf("failed to check pre-commit file status: %s", err) - } - - return nil -} diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go deleted file mode 100644 index 930a27a56..000000000 --- a/cli/packages/cmd/secrets.go +++ /dev/null @@ -1,782 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "fmt" - "os" - "regexp" - "sort" - "strings" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/Infisical/infisical-merge/packages/visualize" - "github.com/posthog/posthog-go" - "github.com/spf13/cobra" -) - -var secretsCmd = &cobra.Command{ - Example: `infisical secrets`, - Short: "Used to create, read update and delete secrets", - Use: "secrets", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - shouldExpandSecrets, err := cmd.Flags().GetBool("expand") - if err != nil { - util.HandleError(err) - } - - includeImports, err := cmd.Flags().GetBool("include-imports") - if err != nil { - util.HandleError(err) - } - - recursive, err := cmd.Flags().GetBool("recursive") - if err != nil { - util.HandleError(err) - } - - tagSlugs, err := cmd.Flags().GetString("tags") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretOverriding, err := cmd.Flags().GetBool("secret-overriding") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - plainOutput, err := cmd.Flags().GetBool("plain") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - request := models.GetAllSecretsParameters{ - Environment: environmentName, - WorkspaceId: projectId, - TagSlugs: tagSlugs, - SecretsPath: secretsPath, - IncludeImport: includeImports, - Recursive: recursive, - ExpandSecretReferences: shouldExpandSecrets, - } - - if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { - request.InfisicalToken = token.Token - } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - request.UniversalAuthAccessToken = token.Token - } - - secrets, err := util.GetAllEnvironmentVariables(request, "") - if err != nil { - util.HandleError(err) - } - - if secretOverriding { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL) - } else { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED) - } - - // Sort the secrets by key so we can create a consistent output - secrets = util.SortSecretsByKeys(secrets) - - if plainOutput { - for _, secret := range secrets { - fmt.Println(fmt.Sprintf("%s=%s", secret.Key, secret.Value)) - } - } else { - visualize.PrintAllSecretDetails(secrets) - } - - Telemetry.CaptureEvent("cli-command:secrets", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) - }, -} - -var secretsGetCmd = &cobra.Command{ - Example: `secrets get ..."`, - Short: "Used to retrieve secrets by name", - Use: "get [secrets]", - DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), - Run: getSecretsByNames, -} - -var secretsGenerateExampleEnvCmd = &cobra.Command{ - Example: `secrets generate-example-env > .example-env`, - Short: "Used to generate a example .env file", - Use: "generate-example-env", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: generateExampleEnv, -} - -var secretsSetCmd = &cobra.Command{ - Example: `secrets set ..."`, - Short: "Used set secrets", - Use: "set [secrets]", - DisableFlagsInUseLine: true, - Args: func(cmd *cobra.Command, args []string) error { - if cmd.Flags().Changed("file") { - if len(args) > 0 { - return fmt.Errorf("secrets cannot be provided as command-line arguments when the --file option is used. Please choose either file-based or argument-based secret input") - } - return nil - } - return cobra.MinimumNArgs(1)(cmd, args) - }, - Run: func(cmd *cobra.Command, args []string) { - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if token == nil && projectId == "" { - _, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretType, err := cmd.Flags().GetString("type") - if err != nil || (secretType != util.SECRET_TYPE_SHARED && secretType != util.SECRET_TYPE_PERSONAL) { - util.HandleError(err, "Unable to parse secret type") - } - - processedArgs := []string{} - for _, arg := range args { - splitKeyValue := strings.SplitN(arg, "=", 2) - if len(splitKeyValue) != 2 { - util.HandleError(fmt.Errorf("invalid argument format: %s. Expected format: key=value or key=@filepath", arg), "") - } - - key := splitKeyValue[0] - value := splitKeyValue[1] - - if strings.HasPrefix(value, "\\@") { - value = "@" + value[2:] - } else if strings.HasPrefix(value, "@") { - filePath := strings.TrimPrefix(value, "@") - content, err := os.ReadFile(filePath) - if err != nil { - util.HandleError(err, fmt.Sprintf("Unable to read file %s", filePath)) - } - value = string(content) - } - - processedArgs = append(processedArgs, fmt.Sprintf("%s=%s", key, value)) - } - - file, err := cmd.Flags().GetString("file") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - var secretOperations []models.SecretSetOperation - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - if projectId == "" { - util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag") - } - - secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file) - } else { - if projectId == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - - projectId = workspaceFile.WorkspaceId - } - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "unable to authenticate [err=%v]") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - - secretOperations, err = util.SetRawSecrets(processedArgs, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{ - Type: "", - Token: loggedInUserDetails.UserCredentials.JTWToken, - }, file) - } - - if err != nil { - util.HandleError(err, "Unable to set secrets") - } - - // Print secret operations - headers := [...]string{"SECRET NAME", "SECRET VALUE", "STATUS"} - rows := [][3]string{} - for _, secretOperation := range secretOperations { - rows = append(rows, [...]string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation}) - } - - visualize.Table(headers, rows) - - Telemetry.CaptureEvent("cli-command:secrets set", posthog.NewProperties().Set("version", util.CLI_VERSION)) - }, -} - -var secretsDeleteCmd = &cobra.Command{ - Example: `secrets delete ..."`, - Short: "Used to delete secrets by name", - Use: "delete [secrets]", - DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretType, err := cmd.Flags().GetString("type") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - - httpClient.SetHeader("Accept", "application/json") - - if projectId == "" { - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - projectId = workspaceFile.WorkspaceId - } - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - httpClient.SetAuthToken(token.Token) - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - - httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken) - } - - for _, secretName := range args { - request := api.DeleteSecretV3Request{ - WorkspaceId: projectId, - Environment: environmentName, - SecretName: secretName, - Type: secretType, - SecretPath: secretsPath, - } - - err = api.CallDeleteSecretsRawV3(httpClient, request) - if err != nil { - util.HandleError(err, "Unable to complete your delete request") - } - } - - fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", ")) - - Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(args)).Set("version", util.CLI_VERSION)) - }, -} - -func getSecretsByNames(cmd *cobra.Command, args []string) { - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - shouldExpand, err := cmd.Flags().GetBool("expand") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - tagSlugs, err := cmd.Flags().GetString("tags") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse path flag") - } - - recursive, err := cmd.Flags().GetBool("recursive") - if err != nil { - util.HandleError(err, "Unable to parse recursive flag") - } - - // deprecated, in favor of --plain - showOnlyValue, err := cmd.Flags().GetBool("raw-value") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - plainOutput, err := cmd.Flags().GetBool("plain") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - includeImports, err := cmd.Flags().GetBool("include-imports") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - secretOverriding, err := cmd.Flags().GetBool("secret-overriding") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - request := models.GetAllSecretsParameters{ - Environment: environmentName, - WorkspaceId: projectId, - TagSlugs: tagSlugs, - SecretsPath: secretsPath, - IncludeImport: includeImports, - Recursive: recursive, - ExpandSecretReferences: shouldExpand, - } - - if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { - request.InfisicalToken = token.Token - } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - request.UniversalAuthAccessToken = token.Token - } - - secrets, err := util.GetAllEnvironmentVariables(request, "") - if err != nil { - util.HandleError(err, "To fetch all secrets") - } - - if secretOverriding { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL) - } else { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED) - } - - requestedSecrets := []models.SingleEnvironmentVariable{} - - secretsMap := getSecretsByKeys(secrets) - - for _, secretKeyFromArg := range args { - if value, ok := secretsMap[secretKeyFromArg]; ok { - requestedSecrets = append(requestedSecrets, value) - } else { - if !(plainOutput || showOnlyValue) { - requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ - Key: secretKeyFromArg, - Type: "*not found*", - Value: "*not found*", - }) - } - } - } - - // showOnlyValue deprecated in favor of --plain, below only for backward compatibility - if plainOutput || showOnlyValue { - for _, secret := range requestedSecrets { - fmt.Println(secret.Value) - } - } else { - visualize.PrintAllSecretDetails(requestedSecrets) - } - - Telemetry.CaptureEvent("cli-command:secrets get", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) -} - -func generateExampleEnv(cmd *cobra.Command, args []string) { - environmentName, _ := cmd.Flags().GetString("env") - if !cmd.Flags().Changed("env") { - environmentFromWorkspace := util.GetEnvFromWorkspaceFile() - if environmentFromWorkspace != "" { - environmentName = environmentFromWorkspace - } - } - - secretsPath, err := cmd.Flags().GetString("path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - tagSlugs, err := cmd.Flags().GetString("tags") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - request := models.GetAllSecretsParameters{ - Environment: environmentName, - WorkspaceId: projectId, - TagSlugs: tagSlugs, - SecretsPath: secretsPath, - IncludeImport: true, - } - - if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { - request.InfisicalToken = token.Token - } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - request.UniversalAuthAccessToken = token.Token - } - - secrets, err := util.GetAllEnvironmentVariables(request, "") - if err != nil { - util.HandleError(err, "To fetch all secrets") - } - - tagsHashToSecretKey := make(map[string]int) - slugsToFilerBy := make(map[string]int) - - for _, slug := range strings.Split(tagSlugs, ",") { - slugsToFilerBy[slug] = 1 - } - - type TagsAndSecrets struct { - Secrets []models.SingleEnvironmentVariable - Tags []struct { - ID string `json:"_id"` - Name string `json:"name"` - Slug string `json:"slug"` - Workspace string `json:"workspace"` - } - } - - // sort secrets by associated tags (most number of tags to least tags) - sort.Slice(secrets, func(i, j int) bool { - return len(secrets[i].Tags) > len(secrets[j].Tags) - }) - - for i, secret := range secrets { - filteredTag := []struct { - ID string "json:\"_id\"" - Name string "json:\"name\"" - Slug string "json:\"slug\"" - Workspace string "json:\"workspace\"" - }{} - - for _, secretTag := range secret.Tags { - _, exists := slugsToFilerBy[secretTag.Slug] - if !exists { - filteredTag = append(filteredTag, secretTag) - } - } - - secret.Tags = filteredTag - secrets[i] = secret - } - - for _, secret := range secrets { - listOfTagSlugs := []string{} - - for _, tag := range secret.Tags { - listOfTagSlugs = append(listOfTagSlugs, tag.Slug) - } - sort.Strings(listOfTagSlugs) - - tagsHash := util.GetHashFromStringList(listOfTagSlugs) - - tagsHashToSecretKey[tagsHash] += 1 - } - - finalTagHashToSecretKey := make(map[string]TagsAndSecrets) - - for _, secret := range secrets { - listOfTagSlugs := []string{} - for _, tag := range secret.Tags { - listOfTagSlugs = append(listOfTagSlugs, tag.Slug) - } - - // sort the slug so we get the same hash each time - sort.Strings(listOfTagSlugs) - - tagsHash := util.GetHashFromStringList(listOfTagSlugs) - occurrence, exists := tagsHashToSecretKey[tagsHash] - if exists && occurrence > 0 { - - value, exists2 := finalTagHashToSecretKey[tagsHash] - allSecretsForTags := append(value.Secrets, secret) - - // sort the the secrets by keys so that they can later be sorted by the first item in the secrets array - sort.Slice(allSecretsForTags, func(i, j int) bool { - return allSecretsForTags[i].Key < allSecretsForTags[j].Key - }) - - if exists2 { - finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{ - Tags: secret.Tags, - Secrets: allSecretsForTags, - } - } else { - finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{ - Tags: secret.Tags, - Secrets: []models.SingleEnvironmentVariable{secret}, - } - } - - tagsHashToSecretKey[tagsHash] -= 1 - } - } - - // sort the fianl result by secret key fo consistent print order - listOfsecretDetails := make([]TagsAndSecrets, 0, len(finalTagHashToSecretKey)) - for _, secretDetails := range finalTagHashToSecretKey { - listOfsecretDetails = append(listOfsecretDetails, secretDetails) - } - - // sort the order of the headings by the order of the secrets - sort.Slice(listOfsecretDetails, func(i, j int) bool { - return len(listOfsecretDetails[i].Tags) < len(listOfsecretDetails[j].Tags) - }) - - tableOfContents := []string{} - fullyGeneratedDocuments := []string{} - for _, secretDetails := range listOfsecretDetails { - listOfKeyValue := []string{} - - for _, secret := range secretDetails.Secrets { - re := regexp.MustCompile(`(?s)(.*)DEFAULT:(.*)`) - match := re.FindStringSubmatch(secret.Comment) - defaultValue := "" - comment := secret.Comment - - // Case: Only has default value - if len(match) == 2 { - defaultValue = strings.TrimSpace(match[1]) - } - - // Case: has a comment and a default value - if len(match) == 3 { - comment = match[1] - defaultValue = match[2] - } - - row := "" - if comment != "" { - comment = addHash(comment) - row = fmt.Sprintf("%s \n%s=%s", strings.TrimSpace(comment), strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue)) - } else { - row = fmt.Sprintf("%s=%s", strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue)) - } - - // each secret row to be added to the file - listOfKeyValue = append(listOfKeyValue, row) - } - - listOfTagNames := []string{} - for _, tag := range secretDetails.Tags { - listOfTagNames = append(listOfTagNames, tag.Name) - } - - heading := CenterString(strings.Join(listOfTagNames, " & "), 80) - - if len(listOfTagNames) == 0 { - fullyGeneratedDocuments = append(fullyGeneratedDocuments, fmt.Sprintf("\n%s \n", strings.Join(listOfKeyValue, "\n"))) - } else { - fullyGeneratedDocuments = append(fullyGeneratedDocuments, fmt.Sprintf("\n\n\n%s \n%s \n", heading, strings.Join(listOfKeyValue, "\n"))) - tableOfContents = append(tableOfContents, strings.ToUpper(strings.Join(listOfTagNames, " & "))) - } - } - - dashedList := []string{} - for _, item := range tableOfContents { - dashedList = append(dashedList, fmt.Sprintf("# - %s \n", item)) - } - if len(dashedList) > 0 { - fmt.Println(CenterString("TABLE OF CONTENTS", 80)) - fmt.Println(strings.Join(dashedList, "")) - } - fmt.Println(strings.Join(fullyGeneratedDocuments, "")) - - Telemetry.CaptureEvent("cli-command:generate-example-env", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) -} - -func CenterString(s string, numStars int) string { - stars := strings.Repeat("*", numStars) - padding := (numStars - len(s)) / 2 - cenetredTextWithStar := stars[:padding] + " " + s + " " + stars[padding:] - - hashes := strings.Repeat("#", len(cenetredTextWithStar)+2) - return fmt.Sprintf("%s \n# %s \n%s", hashes, cenetredTextWithStar, hashes) -} - -func addHash(input string) string { - lines := strings.Split(input, "\n") - for i, line := range lines { - lines[i] = "# " + line - } - return strings.Join(lines, "\n") -} - -func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { - secretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - - for _, secret := range secrets { - secretMapByName[secret.Key] = secret - } - - return secretMapByName -} - -func init() { - secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - secretsGenerateExampleEnvCmd.Flags().String("projectId", "", "manually set the projectId when using machine identity based auth") - secretsGenerateExampleEnvCmd.Flags().String("path", "/", "Fetch secrets from within a folder path") - secretsCmd.AddCommand(secretsGenerateExampleEnvCmd) - - secretsGetCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - secretsGetCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") - secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") - secretsGetCmd.Flags().Bool("plain", false, "print values without formatting, one per line") - secretsGetCmd.Flags().Bool("raw-value", false, "deprecated. Returns only the value of secret, only works with one secret. Use --plain instead") - secretsGetCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") - secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets, and process your referenced secrets") - secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") - secretsGetCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") - secretsCmd.AddCommand(secretsGetCmd) - secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") - secretsCmd.AddCommand(secretsSetCmd) - secretsSetCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth") - secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") - secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared") - secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.") - - secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") - secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - secretsDeleteCmd.Flags().String("projectId", "", "manually set the projectId to delete secrets from when using machine identity based auth") - secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path") - secretsCmd.AddCommand(secretsDeleteCmd) - - // *** Folders sub command *** - folderCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") - - // Add getCmd, createCmd and deleteCmd flags here - getCmd.Flags().StringP("path", "p", "/", "The path from where folders should be fetched from") - getCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - getCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from when using machine identity based auth") - folderCmd.AddCommand(getCmd) - - // Add createCmd flags here - createCmd.Flags().StringP("path", "p", "/", "Path to where the folder should be created") - createCmd.Flags().StringP("name", "n", "", "Name of the folder to be created in selected `--path`") - createCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - createCmd.Flags().String("projectId", "", "manually set the project ID for creating folders in when using machine identity based auth") - folderCmd.AddCommand(createCmd) - - // Add deleteCmd flags here - deleteCmd.Flags().StringP("path", "p", "/", "Path to the folder to be deleted") - deleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - deleteCmd.Flags().String("projectId", "", "manually set the projectId to delete folders when using machine identity based auth") - deleteCmd.Flags().StringP("name", "n", "", "Name of the folder to be deleted within selected `--path`") - folderCmd.AddCommand(deleteCmd) - - secretsCmd.AddCommand(folderCmd) - - // ** End of folders sub command - - secretsCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") - secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets when using machine identity based auth") - secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") - secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets, and process your referenced secrets") - secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") - secretsCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") - secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") - secretsCmd.Flags().String("path", "/", "get secrets within a folder path") - secretsCmd.Flags().Bool("plain", false, "print values without formatting, one per line") - rootCmd.AddCommand(secretsCmd) -} diff --git a/cli/packages/cmd/ssh.go b/cli/packages/cmd/ssh.go deleted file mode 100644 index 4315989bd..000000000 --- a/cli/packages/cmd/ssh.go +++ /dev/null @@ -1,1142 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "context" - "fmt" - "net" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/util" - infisicalSdk "github.com/infisical/go-sdk" - infisicalSdkUtil "github.com/infisical/go-sdk/packages/util" - "github.com/manifoldco/promptui" - "github.com/spf13/cobra" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" -) - -var sshCmd = &cobra.Command{ - Example: `infisical ssh`, - Short: "Used to issue SSH credentials", - Use: "ssh", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, -} - -var sshIssueCredentialsCmd = &cobra.Command{ - Example: `ssh issue-credentials`, - Short: "Used to issue SSH credentials against a certificate template", - Use: "issue-credentials", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: issueCredentials, -} - -var sshSignKeyCmd = &cobra.Command{ - Example: `ssh sign-key`, - Short: "Used to sign a SSH public key against a certificate template", - Use: "sign-key", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: signKey, -} - -var sshConnectCmd = &cobra.Command{ - Use: "connect", - Short: "Connect to an SSH host using issued credentials", - Run: sshConnect, -} - -var sshAddHostCmd = &cobra.Command{ - Use: "add-host", - Short: "Register a new SSH host with Infisical", - Run: sshAddHost, -} - -var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{ - infisicalSdkUtil.RSA2048: "id_rsa_2048", - infisicalSdkUtil.RSA4096: "id_rsa_4096", - infisicalSdkUtil.ECDSAP256: "id_ecdsa_p256", - infisicalSdkUtil.ECDSAP384: "id_ecdsa_p384", -} - -func isValidKeyAlgorithm(algo infisicalSdkUtil.CertKeyAlgorithm) bool { - _, exists := algoToFileName[algo] - return exists -} - -func isValidCertType(certType infisicalSdkUtil.SshCertType) bool { - switch certType { - case infisicalSdkUtil.UserCert, infisicalSdkUtil.HostCert: - return true - default: - return false - } -} - -func writeToFile(filePath string, content string, perm os.FileMode) error { - // Ensure the directory exists - dir := filepath.Dir(filePath) - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %w", dir, err) - } - - // Write the content to the file - err := os.WriteFile(filePath, []byte(content), perm) - if err != nil { - return fmt.Errorf("failed to write to file %s: %w", filePath, err) - } - - return nil -} - -func addCredentialsToAgent(privateKeyContent, certContent string) error { - // Parse the private key - privateKey, err := ssh.ParseRawPrivateKey([]byte(privateKeyContent)) - if err != nil { - return fmt.Errorf("failed to parse private key: %w", err) - } - - // Parse the certificate - pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certContent)) - if err != nil { - return fmt.Errorf("failed to parse certificate: %w", err) - } - - cert, ok := pubKey.(*ssh.Certificate) - if !ok { - return fmt.Errorf("parsed key is not a certificate") - } - // Calculate LifetimeSecs based on certificate's valid-to time - validUntil := time.Unix(int64(cert.ValidBefore), 0) - now := time.Now() - - // Handle ValidBefore as either a timestamp or an enumeration - // SSH certificates use ValidBefore as a timestamp unless set to 0 or ~0 - if cert.ValidBefore == ssh.CertTimeInfinity { - // If certificate never expires, set default lifetime to 1 year (can adjust as needed) - validUntil = now.Add(365 * 24 * time.Hour) - } - - // Calculate the duration until expiration - lifetime := validUntil.Sub(now) - if lifetime <= 0 { - return fmt.Errorf("certificate is already expired") - } - - // Convert duration to seconds - lifetimeSecs := uint32(lifetime.Seconds()) - - // Connect to the SSH agent - socket := os.Getenv("SSH_AUTH_SOCK") - if socket == "" { - return fmt.Errorf("SSH_AUTH_SOCK not set") - } - - conn, err := net.Dial("unix", socket) - if err != nil { - return fmt.Errorf("failed to connect to SSH agent: %w", err) - } - defer conn.Close() - - agentClient := agent.NewClient(conn) - - // Add the key with certificate to the agent - err = agentClient.Add(agent.AddedKey{ - PrivateKey: privateKey, - Certificate: cert, - Comment: "Added via Infisical CLI", - LifetimeSecs: lifetimeSecs, - }) - if err != nil { - return fmt.Errorf("failed to add key to agent: %w", err) - } - - return nil -} - -func issueCredentials(cmd *cobra.Command, args []string) { - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - var infisicalToken string - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - if certificateTemplateId == "" { - util.PrintErrorMessageAndExit("You must set the --certificateTemplateId flag") - } - - principalsStr, err := cmd.Flags().GetString("principals") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - // Check if the input string is empty before splitting - if principalsStr == "" { - util.HandleError(fmt.Errorf("no principals provided"), "The 'principals' flag cannot be empty") - } - - // Convert the comma-delimited string into a slice of strings - principals := strings.Split(principalsStr, ",") - for i, principal := range principals { - principals[i] = strings.TrimSpace(principal) - } - - keyAlgorithm, err := cmd.Flags().GetString("keyAlgorithm") - if err != nil { - util.HandleError(err, "Unable to parse keyAlgorithm flag") - } - - if !isValidKeyAlgorithm(infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)) { - util.HandleError(fmt.Errorf("invalid keyAlgorithm: %s", keyAlgorithm), - "Valid values: RSA_2048, RSA_4096, EC_prime256v1, EC_secp384r1") - } - - certType, err := cmd.Flags().GetString("certType") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if !isValidCertType(infisicalSdkUtil.SshCertType(certType)) { - util.HandleError(fmt.Errorf("invalid certType: %s", certType), - "Valid values: user, host") - } - - ttl, err := cmd.Flags().GetString("ttl") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - keyId, err := cmd.Flags().GetString("keyId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - outFilePath, err := cmd.Flags().GetString("outFilePath") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - addToAgent, err := cmd.Flags().GetBool("addToAgent") - if err != nil { - util.HandleError(err, "Unable to parse addToAgent flag") - } - - if outFilePath == "" && !addToAgent { - util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command") - } - - var ( - outputDir string - privateKeyPath string - publicKeyPath string - signedKeyPath string - ) - - if outFilePath != "" { - // Expand ~ to home directory if present - if strings.HasPrefix(outFilePath, "~") { - homeDir, err := os.UserHomeDir() - if err != nil { - util.HandleError(err, "Failed to resolve home directory") - } - outFilePath = strings.Replace(outFilePath, "~", homeDir, 1) - } - - // Check if outFilePath ends with "-cert.pub" - if strings.HasSuffix(outFilePath, "-cert.pub") { - // Treat outFilePath as the signed key path - signedKeyPath = outFilePath - - // Derive the base name by removing "-cert.pub" - baseName := strings.TrimSuffix(filepath.Base(outFilePath), "-cert.pub") - - // Set the output directory - outputDir = filepath.Dir(outFilePath) - - // Define private and public key paths - privateKeyPath = filepath.Join(outputDir, baseName) - publicKeyPath = filepath.Join(outputDir, baseName+".pub") - } else { - // Treat outFilePath as a directory - outputDir = outFilePath - - // Check if the directory exists; if not, create it - info, err := os.Stat(outputDir) - if os.IsNotExist(err) { - err = os.MkdirAll(outputDir, 0755) - if err != nil { - util.HandleError(err, "Failed to create output directory") - } - } else if err != nil { - util.HandleError(err, "Failed to access output directory") - } else if !info.IsDir() { - util.PrintErrorMessageAndExit("The provided --outFilePath is not a directory") - } - } - } - - // Define file names based on key algorithm - fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)] - - // Define file paths - privateKeyPath = filepath.Join(outputDir, fileName) - publicKeyPath = filepath.Join(outputDir, fileName+".pub") - signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub") - - // If outFilePath ends with "-cert.pub", ensure the signedKeyPath is set - if strings.HasSuffix(outFilePath, "-cert.pub") { - // Ensure the signedKeyPath was set - if signedKeyPath == "" { - util.HandleError(fmt.Errorf("signedKeyPath is not set correctly"), "Internal error") - } - } else { - // Ensure all paths are set - if privateKeyPath == "" || publicKeyPath == "" || signedKeyPath == "" { - util.HandleError(fmt.Errorf("file paths are not set correctly"), "Internal error") - } - } - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - infisicalClient.Auth().SetAccessToken(infisicalToken) - - creds, err := infisicalClient.Ssh().IssueCredentials(infisicalSdk.IssueSshCredsOptions{ - CertificateTemplateID: certificateTemplateId, - Principals: principals, - KeyAlgorithm: infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm), - CertType: infisicalSdkUtil.SshCertType(certType), - TTL: ttl, - KeyID: keyId, - }) - - if err != nil { - util.HandleError(err, "Failed to issue SSH credentials") - } - - if outFilePath != "" { - // If signedKeyPath wasn't set in the directory scenario, set it now - if signedKeyPath == "" { - fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)] - signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub") - } - - if privateKeyPath == "" { - privateKeyPath = filepath.Join(outputDir, algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)]) - } - err = writeToFile(privateKeyPath, creds.PrivateKey, 0600) - if err != nil { - util.HandleError(err, "Failed to write Private Key to file") - } - - if publicKeyPath == "" { - publicKeyPath = privateKeyPath + ".pub" - } - err = writeToFile(publicKeyPath, creds.PublicKey, 0644) - if err != nil { - util.HandleError(err, "Failed to write Public Key to file") - } - - err = writeToFile(signedKeyPath, creds.SignedKey, 0644) - if err != nil { - util.HandleError(err, "Failed to write Signed Key to file") - } - - fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath) - } - - // Add SSH credentials to the SSH agent if needed - if addToAgent { - // Call the helper function to handle add-to-agent flow - err := addCredentialsToAgent(creds.PrivateKey, creds.SignedKey) - if err != nil { - util.HandleError(err, "Failed to add keys to SSH agent") - } else { - fmt.Println("The SSH key and certificate have been successfully added to your ssh-agent.") - } - } -} - -func signKey(cmd *cobra.Command, args []string) { - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - var infisicalToken string - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - if certificateTemplateId == "" { - util.PrintErrorMessageAndExit("You must set the --certificateTemplateId flag") - } - - publicKey, err := cmd.Flags().GetString("publicKey") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - publicKeyFilePath, err := cmd.Flags().GetString("publicKeyFilePath") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if publicKey == "" && publicKeyFilePath == "" { - util.HandleError(fmt.Errorf("either --publicKey or --publicKeyFilePath must be provided"), "Invalid input") - } - - if publicKey != "" && publicKeyFilePath != "" { - util.HandleError(fmt.Errorf("only one of --publicKey or --publicKeyFile can be provided"), "Invalid input") - } - - if publicKeyFilePath != "" { - if strings.HasPrefix(publicKeyFilePath, "~") { - // Expand the tilde (~) to the user's home directory - homeDir, err := os.UserHomeDir() - if err != nil { - util.HandleError(err, "Failed to resolve home directory") - } - publicKeyFilePath = strings.Replace(publicKeyFilePath, "~", homeDir, 1) - } - - // Ensure the file has a .pub extension - if !strings.HasSuffix(publicKeyFilePath, ".pub") { - util.HandleError(fmt.Errorf("public key file must have a .pub extension"), "Invalid input") - } - - content, err := os.ReadFile(publicKeyFilePath) - if err != nil { - util.HandleError(err, "Failed to read public key file") - } - - publicKey = strings.TrimSpace(string(content)) - } - - if strings.TrimSpace(publicKey) == "" { - util.HandleError(fmt.Errorf("Public key is empty"), "Invalid input") - } - - principalsStr, err := cmd.Flags().GetString("principals") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - // Check if the input string is empty before splitting - if principalsStr == "" { - util.HandleError(fmt.Errorf("no principals provided"), "The 'principals' flag cannot be empty") - } - - // Convert the comma-delimited string into a slice of strings - principals := strings.Split(principalsStr, ",") - for i, principal := range principals { - principals[i] = strings.TrimSpace(principal) - } - - certType, err := cmd.Flags().GetString("certType") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if !isValidCertType(infisicalSdkUtil.SshCertType(certType)) { - util.HandleError(fmt.Errorf("invalid certType: %s", certType), - "Valid values: user, host") - } - - ttl, err := cmd.Flags().GetString("ttl") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - keyId, err := cmd.Flags().GetString("keyId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - outFilePath, err := cmd.Flags().GetString("outFilePath") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - var ( - outputDir string - signedKeyPath string - ) - - if outFilePath == "" { - // Use current working directory - if err != nil { - util.HandleError(err, "Failed to get current working directory") - } - - // check if public key path exists - if publicKeyFilePath == "" { - util.PrintErrorMessageAndExit("--outFilePath must be specified when --publicKeyFilePath is not provided") - } - - outputDir = filepath.Dir(publicKeyFilePath) - // Derive the base name by removing "-cert.pub" - baseName := strings.TrimSuffix(filepath.Base(publicKeyFilePath), ".pub") - signedKeyPath = filepath.Join(outputDir, baseName+"-cert.pub") - } else { - // Expand ~ to home directory if present - if strings.HasPrefix(outFilePath, "~") { - homeDir, err := os.UserHomeDir() - if err != nil { - util.HandleError(err, "Failed to resolve home directory") - } - outFilePath = strings.Replace(outFilePath, "~", homeDir, 1) - } - - // Check if outFilePath ends with "-cert.pub" - if !strings.HasSuffix(outFilePath, "-cert.pub") { - util.PrintErrorMessageAndExit("--outFilePath must end with -cert.pub") - } - - // Extract the directory from outFilePath - outputDir = filepath.Dir(outFilePath) - - // Validate the output directory - info, err := os.Stat(outputDir) - if os.IsNotExist(err) { - // Directory does not exist; attempt to create it - err = os.MkdirAll(outputDir, 0755) - if err != nil { - util.HandleError(err, "Failed to create output directory") - } - } else if err != nil { - // Other errors accessing the directory - util.HandleError(err, "Failed to access output directory") - } else if !info.IsDir() { - // Path exists but is not a directory - util.PrintErrorMessageAndExit("The provided --outFilePath's directory is not valid") - } - - signedKeyPath = outFilePath - } - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - infisicalClient.Auth().SetAccessToken(infisicalToken) - - creds, err := infisicalClient.Ssh().SignKey(infisicalSdk.SignSshPublicKeyOptions{ - CertificateTemplateID: certificateTemplateId, - PublicKey: publicKey, - Principals: principals, - CertType: infisicalSdkUtil.SshCertType(certType), - TTL: ttl, - KeyID: keyId, - }) - - if err != nil { - util.HandleError(err, "Failed to sign SSH public key") - } - - err = writeToFile(signedKeyPath, creds.SignedKey, 0644) - if err != nil { - util.HandleError(err, "Failed to write Signed Key to file") - } - - fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath) -} - -func sshConnect(cmd *cobra.Command, args []string) { - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - var infisicalToken string - - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - writeHostCaToFile, err := cmd.Flags().GetBool("write-host-ca-to-file") - if err != nil { - util.HandleError(err, "Unable to parse --write-host-ca-to-file flag") - } - - outFilePath, err := cmd.Flags().GetString("out-file-path") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - hostname, _ := cmd.Flags().GetString("hostname") - loginUser, _ := cmd.Flags().GetString("login-user") - - var outputDir, privateKeyPath, publicKeyPath, signedKeyPath string - if outFilePath != "" { - if strings.HasPrefix(outFilePath, "~") { - homeDir, err := os.UserHomeDir() - if err != nil { - util.HandleError(err, "Failed to resolve home directory") - } - outFilePath = strings.Replace(outFilePath, "~", homeDir, 1) - } - - if strings.HasSuffix(outFilePath, "-cert.pub") { - signedKeyPath = outFilePath - baseName := strings.TrimSuffix(filepath.Base(outFilePath), "-cert.pub") - outputDir = filepath.Dir(outFilePath) - privateKeyPath = filepath.Join(outputDir, baseName) - publicKeyPath = filepath.Join(outputDir, baseName+".pub") - } else { - outputDir = outFilePath - info, err := os.Stat(outputDir) - if os.IsNotExist(err) { - err = os.MkdirAll(outputDir, 0755) - if err != nil { - util.HandleError(err, "Failed to create output directory") - } - } else if err != nil { - util.HandleError(err, "Failed to access output directory") - } else if !info.IsDir() { - util.PrintErrorMessageAndExit("The provided --outFilePath is not a directory") - } - fileName := "id_ed25519" - privateKeyPath = filepath.Join(outputDir, fileName) - publicKeyPath = filepath.Join(outputDir, fileName+".pub") - signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub") - } - - if privateKeyPath == "" || publicKeyPath == "" || signedKeyPath == "" { - util.PrintErrorMessageAndExit("Failed to resolve file paths for writing credentials") - } - } - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - infisicalClient.Auth().SetAccessToken(infisicalToken) - - // Fetch SSH Hosts - hosts, err := infisicalClient.Ssh().GetSshHosts(infisicalSdk.GetSshHostsOptions{}) - if err != nil { - util.HandleError(err, "Failed to fetch SSH hosts") - } - if len(hosts) == 0 { - util.PrintErrorMessageAndExit("You do not have access to any SSH hosts") - } - - var selectedHost = hosts[0] - if hostname != "" { - foundHost := false - for _, h := range hosts { - if h.Hostname == hostname { - selectedHost = h - foundHost = true - break - } - } - if !foundHost { - util.PrintErrorMessageAndExit("Specified --hostname not found or not accessible") - } - } else { - hostNames := make([]string, len(hosts)) - for i, h := range hosts { - if h.Alias != "" { - hostNames[i] = h.Alias - } else { - hostNames[i] = h.Hostname - } - } - - hostPrompt := promptui.Select{ - Label: "Select an SSH Host", - Items: hostNames, - Size: 10, - } - - hostIdx, _, err := hostPrompt.Run() - if err != nil { - util.HandleError(err, "Prompt failed") - } - - selectedHost = hosts[hostIdx] - } - - var selectedLoginUser string - if loginUser != "" { - foundLoginUser := false - for _, m := range selectedHost.LoginMappings { - if m.LoginUser == loginUser { - selectedLoginUser = loginUser - foundLoginUser = true - break - } - } - if !foundLoginUser { - util.PrintErrorMessageAndExit("Specified --loginUser not valid for selected host") - } - } else { - if len(selectedHost.LoginMappings) == 0 { - util.PrintErrorMessageAndExit("No login users available for selected host") - } - loginUsers := make([]string, len(selectedHost.LoginMappings)) - for i, m := range selectedHost.LoginMappings { - loginUsers[i] = m.LoginUser - } - loginPrompt := promptui.Select{ - Label: "Select Login User", - Items: loginUsers, - Size: 5, - } - loginIdx, _, err := loginPrompt.Run() - if err != nil { - util.HandleError(err, "Prompt failed") - } - selectedLoginUser = selectedHost.LoginMappings[loginIdx].LoginUser - } - - // Issue SSH creds for host - creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{ - LoginUser: selectedLoginUser, - }) - if err != nil { - util.HandleError(err, "Failed to issue SSH credentials") - } - - // Write Host CA public key to known_hosts if enabled - if writeHostCaToFile { - hostCaPublicKey, err := infisicalClient.Ssh().GetSshHostHostCaPublicKey(selectedHost.ID) - if err != nil { - util.HandleError(err, "Failed to fetch Host CA public key") - } - - // Build @cert-authority line - caLine := fmt.Sprintf("@cert-authority %s %s\n", selectedHost.Hostname, strings.TrimSpace(hostCaPublicKey)) - - // Determine known_hosts path - sshDir := filepath.Join(os.Getenv("HOME"), ".ssh") - knownHostsPath := filepath.Join(sshDir, "known_hosts") - - // Ensure ~/.ssh exists - if _, err := os.Stat(sshDir); os.IsNotExist(err) { - if err := os.MkdirAll(sshDir, 0700); err != nil { - util.HandleError(err, "Failed to create ~/.ssh directory") - } - } - - // Check if CA line already exists - knownHostsBytes, _ := os.ReadFile(knownHostsPath) - if !strings.Contains(string(knownHostsBytes), caLine) { - f, err := os.OpenFile(knownHostsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - util.HandleError(err, "Failed to open known_hosts file") - } - defer f.Close() - - if _, err := f.WriteString(caLine); err != nil { - util.HandleError(err, "Failed to write Host CA to known_hosts") - } - - fmt.Printf("Successfully wrote Host CA entry to %s\n", knownHostsPath) - } - } - - if outFilePath != "" { - err = writeToFile(privateKeyPath, creds.PrivateKey, 0600) - if err != nil { - util.HandleError(err, "Failed to write private key") - } - err = writeToFile(publicKeyPath, creds.PublicKey, 0644) - if err != nil { - util.HandleError(err, "Failed to write public key") - } - err = writeToFile(signedKeyPath, creds.SignedKey, 0644) - if err != nil { - util.HandleError(err, "Failed to write signed cert") - } - fmt.Printf("Successfully wrote credentials to %s, %s, and %s\n", privateKeyPath, publicKeyPath, signedKeyPath) - return - } - - // Load credentials into SSH agent - err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey) - if err != nil { - util.HandleError(err, "Failed to add credentials to SSH agent") - } - fmt.Println("✔ SSH credentials successfully added to agent") - - // Connect to host using system ssh and agent - target := fmt.Sprintf("%s@%s", selectedLoginUser, selectedHost.Hostname) - fmt.Printf("Connecting to %s...\n", target) - - sshCmd := exec.Command("ssh", target) - sshCmd.Stdin = os.Stdin - sshCmd.Stdout = os.Stdout - sshCmd.Stderr = os.Stderr - - err = sshCmd.Run() - if err != nil { - util.HandleError(err, "SSH connection failed") - } -} - -func sshAddHost(cmd *cobra.Command, args []string) { - - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse token") - } - - var infisicalToken string - if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { - infisicalToken = token.Token - } else { - util.RequireLogin() - - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if err != nil { - util.HandleError(err, "Unable to authenticate") - } - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - infisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - projectId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse --projectId flag") - } - if projectId == "" { - util.PrintErrorMessageAndExit("You must provide --projectId") - } - - hostname, err := cmd.Flags().GetString("hostname") - if err != nil { - util.HandleError(err, "Unable to parse --hostname flag") - } - if hostname == "" { - util.PrintErrorMessageAndExit("You must provide --hostname") - } - - alias, err := cmd.Flags().GetString("alias") - if err != nil { - util.HandleError(err, "Unable to parse --alias flag") - } - - // if alias == "" { - // util.PrintErrorMessageAndExit("You must provide --alias") - // } - - writeUserCaToFile, err := cmd.Flags().GetBool("write-user-ca-to-file") - if err != nil { - util.HandleError(err, "Unable to parse --write-user-ca-to-file flag") - } - - userCaOutFilePath, err := cmd.Flags().GetString("user-ca-out-file-path") - if err != nil { - util.HandleError(err, "Unable to parse --user-ca-out-file-path flag") - } - - writeHostCertToFile, err := cmd.Flags().GetBool("write-host-cert-to-file") - if err != nil { - util.HandleError(err, "Unable to parse --write-host-cert-to-file flag") - } - - configureSshd, err := cmd.Flags().GetBool("configure-sshd") - if err != nil { - util.HandleError(err, "Unable to parse --configure-sshd flag") - } - - forceOverwrite, err := cmd.Flags().GetBool("force") - if err != nil { - util.HandleError(err, "Unable to parse --force flag") - } - - if configureSshd && (!writeUserCaToFile || !writeHostCertToFile) { - util.PrintErrorMessageAndExit("--configure-sshd requires both --write-user-ca-to-file and --write-host-cert-to-file to also be set") - } - - // Pre-check for file overwrites before proceeding - if writeUserCaToFile { - if strings.HasPrefix(userCaOutFilePath, "~") { - homeDir, err := os.UserHomeDir() - if err != nil { - util.HandleError(err, "Unable to resolve ~ in user-ca-out-file-path") - } - userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1) - } - if _, err := os.Stat(userCaOutFilePath); err == nil && !forceOverwrite { - util.PrintErrorMessageAndExit("File already exists at " + userCaOutFilePath + ". Use --force to overwrite.") - } - } - - keyTypes := []string{"ed25519", "ecdsa", "rsa"} - var hostKeyPath, certOutPath, hostPrivateKeyPath string - if writeHostCertToFile { - for _, keyType := range keyTypes { - pub := fmt.Sprintf("/etc/ssh/ssh_host_%s_key.pub", keyType) - cert := fmt.Sprintf("/etc/ssh/ssh_host_%s_key-cert.pub", keyType) - priv := fmt.Sprintf("/etc/ssh/ssh_host_%s_key", keyType) - - if _, err := os.Stat(pub); err == nil { - hostKeyPath = pub - certOutPath = cert - hostPrivateKeyPath = priv - break - } - } - - if hostKeyPath == "" { - util.PrintErrorMessageAndExit("No supported SSH host public key found at /etc/ssh") - } - - if _, err := os.Stat(certOutPath); err == nil && !forceOverwrite { - util.PrintErrorMessageAndExit("File already exists at " + certOutPath + ". Use --force to overwrite.") - } - } - - if configureSshd { - sshdConfig := "/etc/ssh/sshd_config" - existing, err := os.ReadFile(sshdConfig) - if err != nil { - util.HandleError(err, "Failed to read sshd_config") - } - configLines := []string{ - "TrustedUserCAKeys " + userCaOutFilePath, - "HostKey " + hostPrivateKeyPath, - "HostCertificate " + certOutPath, - } - for _, line := range configLines { - for _, existingLine := range strings.Split(string(existing), "\n") { - trimmed := strings.TrimSpace(existingLine) - if trimmed == line && !strings.HasPrefix(trimmed, "#") && !forceOverwrite { - util.PrintErrorMessageAndExit("sshd_config already contains: " + line + ". Use --force to overwrite.") - } - } - } - } - - customHeaders, err := util.GetInfisicalCustomHeadersMap() - if err != nil { - util.HandleError(err, "Unable to get custom headers") - } - - client := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ - SiteUrl: config.INFISICAL_URL, - UserAgent: api.USER_AGENT, - AutoTokenRefresh: false, - CustomHeaders: customHeaders, - }) - client.Auth().SetAccessToken(infisicalToken) - - host, err := client.Ssh().AddSshHost(infisicalSdk.AddSshHostOptions{ - ProjectID: projectId, - Hostname: hostname, - Alias: alias, - }) - if err != nil { - util.HandleError(err, "Failed to register SSH host") - } - - fmt.Println("✅ Successfully registered host:", host.Hostname) - - if writeUserCaToFile { - publicKey, err := client.Ssh().GetSshHostUserCaPublicKey(host.ID) - if err != nil { - util.HandleError(err, "Failed to fetch associated User CA public key") - } - - if err := writeToFile(userCaOutFilePath, publicKey, 0644); err != nil { - util.HandleError(err, "Failed to write User CA public key to file") - } - - fmt.Println("📁 Wrote User CA public key to:", userCaOutFilePath) - } - - if writeHostCertToFile { - pubKeyBytes, err := os.ReadFile(hostKeyPath) - if err != nil { - util.HandleError(err, "Failed to read SSH host public key") - } - res, err := client.Ssh().IssueSshHostHostCert(host.ID, infisicalSdk.IssueSshHostHostCertOptions{ - PublicKey: string(pubKeyBytes), - }) - if err != nil { - util.HandleError(err, "Failed to issue SSH host certificate") - } - if err := writeToFile(certOutPath, res.SignedKey, 0644); err != nil { - util.HandleError(err, "Failed to write SSH host certificate to file") - } - fmt.Println("📁 Wrote host certificate to:", certOutPath) - } - - if configureSshd { - sshdConfig := "/etc/ssh/sshd_config" - contentBytes, err := os.ReadFile(sshdConfig) - if err != nil { - util.HandleError(err, "Failed to read sshd_config") - } - lines := strings.Split(string(contentBytes), "\n") - - configMap := map[string]string{ - "TrustedUserCAKeys": userCaOutFilePath, - "HostKey": hostPrivateKeyPath, - "HostCertificate": certOutPath, - } - - seenKeys := map[string]bool{} - for i, line := range lines { - trimmed := strings.TrimSpace(line) - for key, value := range configMap { - if strings.HasPrefix(trimmed, key+" ") { - seenKeys[key] = true - if strings.HasPrefix(trimmed, "#") || forceOverwrite { - lines[i] = fmt.Sprintf("%s %s", key, value) - } else { - util.PrintErrorMessageAndExit("sshd_config already contains: " + trimmed + ". Use --force to overwrite.") - } - } - } - } - - // Append missing lines - for key, value := range configMap { - if !seenKeys[key] { - lines = append(lines, fmt.Sprintf("%s %s", key, value)) - } - } - - // Write back to file - if err := os.WriteFile(sshdConfig, []byte(strings.Join(lines, "\n")), 0644); err != nil { - util.HandleError(err, "Failed to update sshd_config") - } - fmt.Println("📄 Updated sshd_config entries") - } -} - -func init() { - sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token") - sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for") - sshSignKeyCmd.Flags().String("publicKey", "", "The public key to sign") - sshSignKeyCmd.Flags().String("publicKeyFilePath", "", "The file path to the public key file to sign") - sshSignKeyCmd.Flags().String("outFilePath", "", "The path to write the SSH certificate to such as ~/.ssh/id_rsa-cert.pub. If not provided, the credentials will be saved to the directory of the specified public key file path or the current working directory") - sshSignKeyCmd.Flags().String("principals", "", "The principals that the certificate should be signed for") - sshSignKeyCmd.Flags().String("certType", string(infisicalSdkUtil.UserCert), "The cert type for the created certificate") - sshSignKeyCmd.Flags().String("ttl", "", "The ttl for the created certificate") - sshSignKeyCmd.Flags().String("keyId", "", "The keyId that the created certificate should have") - sshCmd.AddCommand(sshSignKeyCmd) - - sshIssueCredentialsCmd.Flags().String("token", "", "Issue SSH credentials using machine identity access token") - sshIssueCredentialsCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue SSH credentials for") - sshIssueCredentialsCmd.Flags().String("principals", "", "The principals to issue SSH credentials for") - sshIssueCredentialsCmd.Flags().String("keyAlgorithm", string(infisicalSdkUtil.RSA2048), "The key algorithm to issue SSH credentials for") - sshIssueCredentialsCmd.Flags().String("certType", string(infisicalSdkUtil.UserCert), "The cert type to issue SSH credentials for") - sshIssueCredentialsCmd.Flags().String("ttl", "", "The ttl to issue SSH credentials for") - sshIssueCredentialsCmd.Flags().String("keyId", "", "The keyId to issue SSH credentials for") - sshIssueCredentialsCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be saved to the current working directory") - sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent") - sshCmd.AddCommand(sshIssueCredentialsCmd) - - sshConnectCmd.Flags().String("token", "", "Use a machine identity access token") - sshConnectCmd.Flags().Bool("write-host-ca-to-file", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist") - sshConnectCmd.Flags().String("hostname", "", "Hostname of the SSH host to connect to") - sshConnectCmd.Flags().String("login-user", "", "Login user for the SSH connection") - sshConnectCmd.Flags().String("out-file-path", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be added to the SSH agent and used to establish an interactive SSH connection") - sshCmd.AddCommand(sshConnectCmd) - - sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token") - sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)") - sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)") - sshAddHostCmd.Flags().String("alias", "", "Alias for the SSH host") - sshAddHostCmd.Flags().Bool("write-user-ca-to-file", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub") - sshAddHostCmd.Flags().String("user-ca-out-file-path", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key") - sshAddHostCmd.Flags().Bool("write-host-cert-to-file", false, "Write SSH host certificate to /etc/ssh/ssh_host__key-cert.pub") - sshAddHostCmd.Flags().Bool("configure-sshd", false, "Update `TrustedUserCAKeys`, `HostKey`, and `HostCertificate` in the `/etc/ssh/sshd_config` file") - sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of `--write-user-ca-to-file` and `--write-host-cert-to-file`") - - sshCmd.AddCommand(sshAddHostCmd) - - rootCmd.AddCommand(sshCmd) -} diff --git a/cli/packages/cmd/token.go b/cli/packages/cmd/token.go deleted file mode 100644 index 4e568cb85..000000000 --- a/cli/packages/cmd/token.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "strings" - "time" - - "github.com/Infisical/infisical-merge/packages/util" - "github.com/fatih/color" - "github.com/spf13/cobra" -) - -var tokenCmd = &cobra.Command{ - Use: "token", - Short: "Manage your access tokens", - DisableFlagsInUseLine: true, - Example: "infisical token", - Args: cobra.ExactArgs(0), - PreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - }, - Run: func(cmd *cobra.Command, args []string) { - }, -} - -var tokenRenewCmd = &cobra.Command{ - Use: "renew [token]", - Short: "Used to renew your universal auth access token", - DisableFlagsInUseLine: true, - Example: "infisical token renew ", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - // args[0] will be the from your command call - token := args[0] - - if strings.HasPrefix(token, "st.") { - util.PrintErrorMessageAndExit("You are trying to renew a service token. You can only renew universal auth access tokens.") - } - - renewedAccessToken, err := util.RenewMachineIdentityAccessToken(token) - - if err != nil { - util.HandleError(err, "Unable to renew token") - } - - boldGreen := color.New(color.FgGreen).Add(color.Bold) - time.Sleep(time.Second * 1) - boldGreen.Printf(">>>> Successfully renewed token!\n\n") - boldGreen.Printf("Renewed Access Token:\n%v", renewedAccessToken) - - plainBold := color.New(color.Bold) - plainBold.Println("\n\nYou can use the new access token to authenticate through other commands in the CLI.") - - }, -} - -func init() { - tokenCmd.AddCommand(tokenRenewCmd) - - rootCmd.AddCommand(tokenCmd) -} diff --git a/cli/packages/cmd/tokens.go b/cli/packages/cmd/tokens.go deleted file mode 100644 index a2e445239..000000000 --- a/cli/packages/cmd/tokens.go +++ /dev/null @@ -1,193 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "crypto/rand" - "encoding/base64" - "encoding/hex" - "fmt" - "strings" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/crypto" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/spf13/cobra" -) - -var tokensCmd = &cobra.Command{ - Use: "service-token", - Short: "Manage service tokens", - DisableFlagsInUseLine: true, - Example: "infisical service-token", - Args: cobra.ExactArgs(0), - PreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - }, - Run: func(cmd *cobra.Command, args []string) { - }, -} - -var tokensCreateCmd = &cobra.Command{ - Use: "create", - Short: "Used to create service tokens", - DisableFlagsInUseLine: true, - Example: "infisical service-token create", - Args: cobra.ExactArgs(0), - PreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - }, - Run: func(cmd *cobra.Command, args []string) { - // get plain text workspace key - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - - if err != nil { - util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again") - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - - tokenOnly, err := cmd.Flags().GetBool("token-only") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - workspaceId, err := cmd.Flags().GetString("projectId") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if workspaceId == "" { - configFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - workspaceId = configFile.WorkspaceId - } - - serviceTokenName, err := cmd.Flags().GetString("name") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - expireSeconds, err := cmd.Flags().GetInt("expiry-seconds") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - scopes, err := cmd.Flags().GetStringSlice("scope") - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - - if len(scopes) == 0 { - util.PrintErrorMessageAndExit("You must define the environments and paths your service token should have access to via the --scope flag") - } - - permissions := []api.ScopePermission{} - - for _, scope := range scopes { - parts := strings.Split(scope, ":") - - if len(parts) != 2 { - fmt.Println("--scope flag is malformed. Each scope flag should be in the following format: :") - return - } - - permissions = append(permissions, api.ScopePermission{Environment: parts[0], SecretPath: parts[1]}) - } - - accessLevels, err := cmd.Flags().GetStringSlice("access-level") - if err != nil { - util.HandleError(err, "Unable to parse flag accessLevels") - } - - if len(accessLevels) == 0 { - util.PrintErrorMessageAndExit("You must define whether your service token can be used to read and or write via the --access-level flag") - } - - for _, accessLevel := range accessLevels { - if accessLevel != "read" && accessLevel != "write" { - util.PrintErrorMessageAndExit("--access-level can only be of values read and write") - } - } - - workspaceKey, err := util.GetPlainTextWorkspaceKey(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceId) - if err != nil { - util.HandleError(err, "Unable to get workspace key needed to create service token") - } - - newWorkspaceEncryptionKey := make([]byte, 16) - _, err = rand.Read(newWorkspaceEncryptionKey) - if err != nil { - util.HandleError(err) - } - - newWorkspaceEncryptionKeyHexFormat := hex.EncodeToString(newWorkspaceEncryptionKey) - - // encrypt the workspace key symmetrically - encryptedDetails, err := crypto.EncryptSymmetric(workspaceKey, []byte(newWorkspaceEncryptionKeyHexFormat)) - if err != nil { - util.HandleError(err) - } - - // make a call to the api to save the encrypted symmetric key details - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - util.HandleError(err, "Unable to get resty client with custom headers") - } - - httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). - SetHeader("Accept", "application/json") - - createServiceTokenResponse, err := api.CallCreateServiceToken(httpClient, api.CreateServiceTokenRequest{ - Name: serviceTokenName, - WorkspaceId: workspaceId, - Scopes: permissions, - ExpiresIn: expireSeconds, - EncryptedKey: base64.StdEncoding.EncodeToString(encryptedDetails.CipherText), - Iv: base64.StdEncoding.EncodeToString(encryptedDetails.Nonce), - Tag: base64.StdEncoding.EncodeToString(encryptedDetails.AuthTag), - RandomBytes: newWorkspaceEncryptionKeyHexFormat, - Permissions: accessLevels, - }) - - if err != nil { - util.HandleError(err, "Unable to create service token") - } - - serviceToken := createServiceTokenResponse.ServiceToken + "." + newWorkspaceEncryptionKeyHexFormat - - if tokenOnly { - fmt.Println(serviceToken) - } else { - printablePermission := []string{} - for _, permission := range permissions { - printablePermission = append(printablePermission, fmt.Sprintf("([environment: %v] [path: %v])", permission.Environment, permission.SecretPath)) - } - - fmt.Printf("New service token created\n") - fmt.Printf("Name: %v\n", serviceTokenName) - fmt.Printf("Project ID: %v\n", workspaceId) - fmt.Printf("Access type: [%v]\n", strings.Join(accessLevels, ", ")) - fmt.Printf("Permission(s): %v\n", strings.Join(printablePermission, ", ")) - fmt.Printf("Service Token: %v\n", serviceToken) - } - }, -} - -func init() { - tokensCreateCmd.Flags().String("projectId", "", "The project ID you'd like to create the service token for. Default: will use linked Infisical project in .infisical.json") - tokensCreateCmd.Flags().StringSliceP("scope", "s", []string{}, "Environment and secret path. Example format: :") - tokensCreateCmd.Flags().StringP("name", "n", "Service token generated via CLI", "Service token name") - tokensCreateCmd.Flags().StringSliceP("access-level", "a", []string{}, "The type of access the service token should have. Can be 'read' and or 'write'") - tokensCreateCmd.Flags().Bool("token-only", false, "When true, only the service token will be printed") - tokensCreateCmd.Flags().IntP("expiry-seconds", "e", 86400, "Set the service token's expiration time in seconds from now. To never expire set to zero. Default: 1 day ") - - tokensCmd.AddCommand(tokensCreateCmd) - - rootCmd.AddCommand(tokensCmd) -} diff --git a/cli/packages/cmd/user.go b/cli/packages/cmd/user.go deleted file mode 100644 index 3b0970403..000000000 --- a/cli/packages/cmd/user.go +++ /dev/null @@ -1,325 +0,0 @@ -package cmd - -import ( - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "net/url" - "strings" - - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/manifoldco/promptui" - "github.com/posthog/posthog-go" - "github.com/spf13/cobra" -) - -var userCmd = &cobra.Command{ - Use: "user", - Short: "Used to manage local user credentials", - DisableFlagsInUseLine: true, - Example: "infisical user", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, -} - -var switchCmd = &cobra.Command{ - Use: "switch", - Short: "Used to switch between Infisical profiles", - DisableFlagsInUseLine: true, - Example: "infisical switch", - Args: cobra.ExactArgs(0), - PreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - }, - Run: func(cmd *cobra.Command, args []string) { - //get previous logged in profiles - loggedInProfiles, err := getLoggedInUsers() - if err != nil { - util.HandleError(err, "[infisical user switch]: Unable to get logged Profiles") - } - - //prompt user - profile, err := LoggedInUsersPrompt(loggedInProfiles) - if err != nil { - util.HandleError(err, "[infisical user switch]: Prompt error") - } - - //write to config file - configFile, err := util.GetConfigFile() - if err != nil { - util.HandleError(err, "[infisical user switch]: Unable to get config file") - } - - configFile.LoggedInUserEmail = profile - - //set logged in user domain - ok := util.ConfigContainsEmail(configFile.LoggedInUsers, profile) - - if !ok { - //profile not in loggedInUsers - configFile.LoggedInUsers = append(configFile.LoggedInUsers, models.LoggedInUser{ - Email: profile, - Domain: config.INFISICAL_URL, - }) - //set logged in user domain - configFile.LoggedInUserDomain = config.INFISICAL_URL - - } else { - //exists, set logged in user domain - for _, v := range configFile.LoggedInUsers { - if profile == v.Email { - configFile.LoggedInUserDomain = v.Domain - break - } - } - } - - err = util.WriteConfigFile(&configFile) - if err != nil { - util.HandleError(err, "") - } - - Telemetry.CaptureEvent("cli-command:user switch", posthog.NewProperties().Set("numberOfLoggedInProfiles", len(loggedInProfiles)).Set("version", util.CLI_VERSION)) - }, -} - -var userGetCmd = &cobra.Command{ - Use: "get", - Short: "Used to get properties of an Infisical profile", - DisableFlagsInUseLine: true, - Example: "infisical user get", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, -} - -var userGetTokenCmd = &cobra.Command{ - Use: "token", - Short: "Used to get the access token of an Infisical user", - DisableFlagsInUseLine: true, - Example: "infisical user get token", - Args: cobra.ExactArgs(0), - PreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - }, - Run: func(cmd *cobra.Command, args []string) { - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = util.EstablishUserLoginSession() - } - - plain, err := cmd.Flags().GetBool("plain") - if err != nil { - util.HandleError(err, "[infisical user get token]: Unable to get plain flag") - } - - if err != nil { - util.HandleError(err, "[infisical user get token]: Unable to get logged in user token") - } - - tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JTWToken, ".") - if len(tokenParts) != 3 { - util.HandleError(errors.New("invalid token format"), "[infisical user get token]: Invalid token format") - } - - payload, err := base64.RawURLEncoding.DecodeString(tokenParts[1]) - if err != nil { - util.HandleError(err, "[infisical user get token]: Unable to decode token payload") - } - - var tokenPayload struct { - TokenVersionId string `json:"tokenVersionId"` - } - if err := json.Unmarshal(payload, &tokenPayload); err != nil { - util.HandleError(err, "[infisical user get token]: Unable to parse token payload") - } - - if plain { - fmt.Println(loggedInUserDetails.UserCredentials.JTWToken) - } else { - fmt.Println("Session ID:", tokenPayload.TokenVersionId) - fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken) - } - }, -} - -var updateCmd = &cobra.Command{ - Use: "update", - Short: "Used to update properties of an Infisical profile", - DisableFlagsInUseLine: true, - Example: "infisical user update", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, -} - -var domainCmd = &cobra.Command{ - Use: "domain", - Short: "Used to update the domain of an Infisical profile", - DisableFlagsInUseLine: true, - Example: "infisical user update domain", - Args: cobra.ExactArgs(0), - PreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - }, - Run: func(cmd *cobra.Command, args []string) { - //prompt for profiles selection - loggedInProfiles, err := getLoggedInUsers() - if err != nil { - util.HandleError(err, "[infisical user update domain]: Unable to get logged Profiles") - } - - //prompt user - profile, err := LoggedInUsersPrompt(loggedInProfiles) - if err != nil { - util.HandleError(err, "[infisical user update domain]: Prompt error") - } - - domain := "" - domainQuery := true - if config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) && config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) { - - override, err := DomainOverridePrompt() - if err != nil { - util.HandleError(err, "[infisical user update domain]: Domain override prompt error") - } - - if !override { - domainQuery = false - domain = config.INFISICAL_URL_MANUAL_OVERRIDE - } - - } - - if domainQuery { - //prompt to update domain - domain, err = NewDomainPrompt() - if err != nil { - util.HandleError(err, "[infisical user update domain]: Prompt error") - } - } - - //write to config file - configFile, err := util.GetConfigFile() - if err != nil { - util.HandleError(err, "[infisical user update domain]: Unable to get config file") - } - - //check if profile in logged in profiles - - //if not add new profile loggedInUsers - //else update profile from loggedinUsers slice - ok := util.ConfigContainsEmail(configFile.LoggedInUsers, profile) - if !ok { - configFile.LoggedInUsers = append(configFile.LoggedInUsers, models.LoggedInUser{ - Email: profile, - Domain: domain, - }) - } else { - //exists, set logged in user domain - for idx, v := range configFile.LoggedInUsers { - if profile == v.Email { - configFile.LoggedInUsers[idx].Domain = domain //inplace - break - } - } - - } - //check if current loggedinuser is selected profile - //if yes set current domain to changed domain - if configFile.LoggedInUserEmail == profile { - configFile.LoggedInUserDomain = domain - } - - err = util.WriteConfigFile(&configFile) - if err != nil { - util.HandleError(err, "") - } - Telemetry.CaptureEvent("cli-command:user domain", posthog.NewProperties().Set("version", util.CLI_VERSION)) - }, -} - -func init() { - updateCmd.AddCommand(domainCmd) - userCmd.AddCommand(updateCmd) - - userGetTokenCmd.Flags().Bool("plain", false, "print token without formatting") - userGetCmd.AddCommand(userGetTokenCmd) - - userCmd.AddCommand(userGetCmd) - userCmd.AddCommand(switchCmd) - rootCmd.AddCommand(userCmd) -} - -// This returns all logged in user emails from the config file. -// If none, it returns the current logged in user in a slice -func getLoggedInUsers() ([]string, error) { - loggedInProfiles := []string{} - - if util.ConfigFileExists() { - configFile, err := util.GetConfigFile() - if err != nil { - return loggedInProfiles, err - } - - //get logged in profiles - // - if len(configFile.LoggedInUsers) > 0 { - for _, v := range configFile.LoggedInUsers { - loggedInProfiles = append(loggedInProfiles, v.Email) - } - } else { - - loggedInProfiles = append(loggedInProfiles, configFile.LoggedInUserEmail) - } - return loggedInProfiles, nil - } else { - //empty - return loggedInProfiles, errors.New("couldn't retrieve config file") - } -} - -func NewDomainPrompt() (string, error) { - urlValidation := func(input string) error { - _, err := url.ParseRequestURI(input) - if err != nil { - return errors.New("this is an invalid url") - } - return nil - } - - //else run prompt to enter domain - domainPrompt := promptui.Prompt{ - Label: "New Domain", - Validate: urlValidation, - Default: "Example - https://my-self-hosted-instance.com/api", - } - - domain, err := domainPrompt.Run() - if err != nil { - return "", err - } - - return util.AppendAPIEndpoint(domain), nil -} - -func LoggedInUsersPrompt(profiles []string) (string, error) { - prompt := promptui.Select{Label: "Which of your Infisical profiles would you like to use", - Items: profiles, - Size: 7, - } - - idx, _, err := prompt.Run() - if err != nil { - return "", err - } - - return profiles[idx], nil -} diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go deleted file mode 100644 index 6a92ef960..000000000 --- a/cli/packages/cmd/vault.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright (c) 2023 Infisical Inc. -*/ -package cmd - -import ( - "encoding/base64" - "fmt" - "strings" - - "github.com/Infisical/infisical-merge/packages/util" - "github.com/posthog/posthog-go" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -type VaultBackendType struct { - Name string - Description string -} - -var AvailableVaults = []VaultBackendType{ - { - Name: "auto", - Description: "automatically select the system keyring", - }, - { - Name: "file", - Description: "encrypted file vault", - }, -} - -var vaultSetCmd = &cobra.Command{ - Example: `infisical vault set file`, - Use: "set [file|auto]", - Short: "Used to configure the vault backends", - DisableFlagsInUseLine: true, - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { - wantedVaultTypeName := args[0] - currentVaultBackend, err := util.GetCurrentVaultBackend() - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) - return - } - - if wantedVaultTypeName == string(currentVaultBackend) { - log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend) - return - } - - if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE { - configFile, err := util.GetConfigFile() - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) - return - } - - configFile.VaultBackendType = wantedVaultTypeName - configFile.LoggedInUserEmail = "" - configFile.VaultBackendPassphrase = base64.StdEncoding.EncodeToString([]byte(util.GenerateRandomString(10))) - - err = util.WriteConfigFile(&configFile) - if err != nil { - log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err) - return - } - - fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName) - - Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION)) - } else { - var availableVaultsNames []string - for _, vault := range AvailableVaults { - availableVaultsNames = append(availableVaultsNames, vault.Name) - } - log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", ")) - } - }, -} - -// runCmd represents the run command -var vaultCmd = &cobra.Command{ - Use: "vault", - Short: "Used to manage where your Infisical login token is saved on your machine", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - printAvailableVaultBackends() - }, -} - -func printAvailableVaultBackends() { - fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:") - for _, vaultType := range AvailableVaults { - fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description) - } - - currentVaultBackend, err := util.GetCurrentVaultBackend() - if err != nil { - log.Error().Msgf("printAvailableVaultBackends: unable to print the available vault backend because of error [err=%s]", err) - } - - Telemetry.CaptureEvent("cli-command:vault", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("version", util.CLI_VERSION)) - - fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend)) -} - -func init() { - vaultCmd.AddCommand(vaultSetCmd) - - rootCmd.AddCommand(vaultCmd) -} diff --git a/cli/packages/config/config.go b/cli/packages/config/config.go deleted file mode 100644 index c5e162c92..000000000 --- a/cli/packages/config/config.go +++ /dev/null @@ -1,5 +0,0 @@ -package config - -var INFISICAL_URL string -var INFISICAL_URL_MANUAL_OVERRIDE string -var INFISICAL_LOGIN_URL string diff --git a/cli/packages/crypto/crypto.go b/cli/packages/crypto/crypto.go deleted file mode 100644 index 2f507ed4c..000000000 --- a/cli/packages/crypto/crypto.go +++ /dev/null @@ -1,86 +0,0 @@ -package crypto - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "io" - - "github.com/Infisical/infisical-merge/packages/models" - "golang.org/x/crypto/nacl/box" -) - -// will decrypt cipher text to plain text using iv and tag -func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) { - // Case: empty string - if len(cipherText) == 0 && len(tag) == 0 && len(iv) == 0 { - return []byte{}, nil - } - - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - aesgcm, err := cipher.NewGCMWithNonceSize(block, len(iv)) - if err != nil { - return nil, err - } - - var nonce = iv - var ciphertext = append(cipherText, tag...) // the aesgcm open method expects auth tag at the end of the cipher text - - plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, err - } - - return plaintext, nil -} - -func GenerateNewKey() (newKey []byte, keyErr error) { - key := make([]byte, 16) // block size defaults to 16 so this is fine - _, err := rand.Read(key) - return key, err -} - -// Will encrypt a plain text with the provided key -func EncryptSymmetric(plaintext []byte, key []byte) (result models.SymmetricEncryptionResult, err error) { - block, err := aes.NewCipher(key) - if err != nil { - return models.SymmetricEncryptionResult{}, err - } - - aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) // default is 12, 16 because https://github.com/Infisical/infisical/blob/bea0ff6e05a4de73a5db625d4ae181a015b50855/backend/src/utils/aes-gcm.ts#L4 - if err != nil { - return models.SymmetricEncryptionResult{}, err - } - - // create a nonce - nonce := make([]byte, aesgcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - panic(err) - } - - ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) - - ciphertextOnly := ciphertext[:len(ciphertext)-16] // combines the auth tag with the cipher text so we need to extract it - - authTag := ciphertext[len(ciphertext)-16:] - - return models.SymmetricEncryptionResult{ - CipherText: ciphertextOnly, - AuthTag: authTag, - Nonce: nonce, - }, nil -} - -func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) { - plainTextToReturn, _ := box.Open(nil, ciphertext, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) - return plainTextToReturn -} - -func EncryptAssymmetric(message []byte, nonce []byte, publicKey []byte, privateKey []byte) (encryptedMessage []byte) { - encryptedPlainText := box.Seal(nil, message, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) - return encryptedPlainText -} diff --git a/cli/packages/gateway/connection.go b/cli/packages/gateway/connection.go deleted file mode 100644 index 980137374..000000000 --- a/cli/packages/gateway/connection.go +++ /dev/null @@ -1,358 +0,0 @@ -package gateway - -import ( - "bufio" - "bytes" - "context" - "crypto/tls" - "crypto/x509" - "encoding/base64" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "os" - "strings" - "sync" - "time" - - "github.com/quic-go/quic-go" - "github.com/rs/zerolog/log" -) - -func handleConnection(ctx context.Context, quicConn quic.Connection) { - log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String()) - // Use WaitGroup to track all streams - var wg sync.WaitGroup - - contextWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - - for { - // Accept the first stream, which we'll use for commands - stream, err := quicConn.AcceptStream(contextWithTimeout) - if err != nil { - log.Printf("Failed to accept QUIC stream: %v", err) - break - } - wg.Add(1) - go func(stream quic.Stream) { - defer wg.Done() - defer stream.Close() - - handleStream(stream, quicConn) - }(stream) - } - - wg.Wait() - log.Printf("All streams closed for connection: %s", quicConn.RemoteAddr().String()) -} - -func handleStream(stream quic.Stream, quicConn quic.Connection) { - streamID := stream.StreamID() - log.Printf("New stream %d from: %s", streamID, quicConn.RemoteAddr().String()) - - // Use buffered reader for better handling of fragmented data - reader := bufio.NewReader(stream) - defer func() { - log.Info().Msgf("Closing stream %d", streamID) - if stream != nil { - stream.Close() - } - }() - - for { - msg, err := reader.ReadBytes('\n') - if err != nil { - if errors.Is(err, io.EOF) { - return - } - log.Error().Msgf("Error reading command: %s", err) - return - } - - cmd := bytes.ToUpper(bytes.TrimSpace(bytes.Split(msg, []byte(" "))[0])) - args := bytes.TrimSpace(bytes.TrimPrefix(msg, cmd)) - - switch string(cmd) { - case "FORWARD-TCP": - proxyAddress := string(bytes.Split(args, []byte(" "))[0]) - destTarget, err := net.Dial("tcp", proxyAddress) - if err != nil { - log.Error().Msgf("Failed to connect to target: %v", err) - return - } - defer destTarget.Close() - log.Info().Msgf("Starting secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String()) - - // Handle buffered data - buffered := reader.Buffered() - if buffered > 0 { - bufferedData := make([]byte, buffered) - _, err := reader.Read(bufferedData) - if err != nil { - log.Error().Msgf("Error reading buffered data: %v", err) - return - } - - if _, err = destTarget.Write(bufferedData); err != nil { - log.Error().Msgf("Error writing buffered data: %v", err) - return - } - } - - CopyDataFromQuicToTcp(stream, destTarget) - log.Info().Msgf("Ending secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String()) - return - - case "FORWARD-HTTP": - targetURL := "" - argParts := bytes.Split(args, []byte(" ")) - - if len(argParts) == 0 || len(argParts[0]) == 0 { - log.Warn().Msg("FORWARD-HTTP used without a target URL.") - } else { - targetURL = string(argParts[0]) - if !isValidURL(targetURL) { - log.Error().Msgf("Invalid target URL: %s", targetURL) - return - } - } - - // Parse optional parameters - var caCertB64, verifyParam string - for _, part := range argParts[1:] { - partStr := string(part) - if strings.HasPrefix(partStr, "ca=") { - caCertB64 = strings.TrimPrefix(partStr, "ca=") - } else if strings.HasPrefix(partStr, "verify=") { - verifyParam = strings.TrimPrefix(partStr, "verify=") - } - } - - log.Info().Msgf("Starting HTTP proxy to: %s", targetURL) - - if err := handleHTTPProxy(stream, reader, targetURL, caCertB64, verifyParam); err != nil { - log.Error().Msgf("HTTP proxy error: %v", err) - } - return - - case "PING": - if _, err := stream.Write([]byte("PONG\n")); err != nil { - log.Error().Msgf("Error writing PONG response: %v", err) - } - return - default: - log.Error().Msgf("Unknown command: %s", string(cmd)) - return - } - } -} -func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string, caCertB64 string, verifyParam string) error { - transport := &http.Transport{ - DisableKeepAlives: false, - MaxIdleConns: 10, - IdleConnTimeout: 30 * time.Second, - } - - if strings.HasPrefix(targetURL, "https://") { - tlsConfig := &tls.Config{} - - if caCertB64 != "" { - caCert, err := base64.StdEncoding.DecodeString(caCertB64) - if err == nil { - caCertPool := x509.NewCertPool() - if caCertPool.AppendCertsFromPEM(caCert) { - tlsConfig.RootCAs = caCertPool - log.Info().Msg("Using provided CA certificate from gateway client") - } else { - log.Error().Msg("Failed to parse provided CA certificate") - } - } else { - log.Error().Msgf("Failed to decode CA certificate: %v", err) - } - } - - if verifyParam != "" { - tlsConfig.InsecureSkipVerify = verifyParam == "false" - log.Info().Msgf("TLS verification set to: %s", verifyParam) - } - - transport.TLSClientConfig = tlsConfig - } - - // Loop to handle multiple HTTP requests on the same stream - for { - req, err := http.ReadRequest(reader) - - if err != nil { - if errors.Is(err, io.EOF) { - log.Info().Msg("Client closed HTTP connection") - return nil - } - return fmt.Errorf("failed to read HTTP request: %v", err) - } - log.Info().Msgf("Received HTTP request: %s", req.URL.Path) - - actionHeader := HttpProxyAction(req.Header.Get(INFISICAL_HTTP_PROXY_ACTION_HEADER)) - if actionHeader != "" { - if actionHeader == HttpProxyActionInjectGatewayK8sServiceAccountToken { - token, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH) - if err != nil { - stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token"))) - continue // Continue to next request instead of returning - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token))) - log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL) - } else if actionHeader == HttpProxyActionUseGatewayK8sServiceAccount { // will work without a target URL set - // set the ca cert to the pod's k8s service account ca cert: - caCert, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH) - if err != nil { - stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa ca cert"))) - continue - } - - caCertPool := x509.NewCertPool() - if ok := caCertPool.AppendCertsFromPEM(caCert); !ok { - stream.Write([]byte(buildHttpInternalServerError("failed to parse k8s sa ca cert"))) - continue - } - - transport.TLSClientConfig = &tls.Config{ - RootCAs: caCertPool, - } - - // set authorization header to the pod's k8s service account token: - token, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH) - if err != nil { - stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token"))) - continue - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token))) - - // update the target URL to point to the kubernetes API server: - kubernetesServiceHost := os.Getenv(KUBERNETES_SERVICE_HOST_ENV_NAME) - kubernetesServicePort := os.Getenv(KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME) - - fullBaseUrl := fmt.Sprintf("https://%s:%s", kubernetesServiceHost, kubernetesServicePort) - targetURL = fullBaseUrl - - log.Info().Msgf("Redirected request to Kubernetes API server: %s", targetURL) - } - - req.Header.Del(INFISICAL_HTTP_PROXY_ACTION_HEADER) - } - - // Build full target URL - var targetFullURL string - if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") { - baseURL := strings.TrimSuffix(targetURL, "/") - targetFullURL = baseURL + req.URL.Path - if req.URL.RawQuery != "" { - targetFullURL += "?" + req.URL.RawQuery - } - } else { - baseURL := strings.TrimSuffix("http://"+targetURL, "/") - targetFullURL = baseURL + req.URL.Path - if req.URL.RawQuery != "" { - targetFullURL += "?" + req.URL.RawQuery - } - } - - // create the request to the target - proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body) - if err != nil { - log.Error().Msgf("Failed to create proxy request: %v", err) - stream.Write([]byte(buildHttpInternalServerError("failed to create proxy request"))) - continue // Continue to next request - } - proxyReq.Header = req.Header.Clone() - - log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL) - - client := &http.Client{ - Transport: transport, - Timeout: 30 * time.Second, - } - - resp, err := client.Do(proxyReq) - if err != nil { - log.Error().Msgf("Failed to reach target: %v", err) - stream.Write([]byte(buildHttpInternalServerError(fmt.Sprintf("failed to reach target due to networking error: %s", err.Error())))) - continue // Continue to next request - } - - // Write the entire response (status line, headers, body) to the stream - // http.Response.Write handles this for "Connection: close" correctly. - // For other connection tokens, manual removal might be needed if they cause issues with QUIC. - // For a simple proxy, this is generally sufficient. - resp.Header.Del("Connection") // Good practice for proxies - - log.Info().Msgf("Writing response to stream: %s", resp.Status) - - if err := resp.Write(stream); err != nil { - log.Error().Err(err).Msg("Failed to write response to stream") - resp.Body.Close() - return fmt.Errorf("failed to write response to stream: %w", err) - } - - resp.Body.Close() - - // Check if client wants to close connection - if req.Header.Get("Connection") == "close" { - log.Info().Msg("Client requested connection close") - return nil - } - } -} - -func buildHttpInternalServerError(message string) string { - return fmt.Sprintf("HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}", message) -} - -type CloseWrite interface { - CloseWrite() error -} - -func isValidURL(str string) bool { - u, err := url.Parse(str) - return err == nil && u.Scheme != "" && u.Host != "" -} - -func CopyDataFromQuicToTcp(quicStream quic.Stream, tcpConn net.Conn) { - // Create a WaitGroup to wait for both copy operations - var wg sync.WaitGroup - wg.Add(2) - - // Start copying from QUIC stream to TCP - go func() { - defer wg.Done() - if _, err := io.Copy(tcpConn, quicStream); err != nil { - log.Error().Msgf("Error copying quic->postgres: %v", err) - } - - if e, ok := tcpConn.(CloseWrite); ok { - log.Debug().Msg("Closing TCP write end") - e.CloseWrite() - } else { - log.Debug().Msg("TCP connection does not support CloseWrite") - } - }() - - // Start copying from TCP to QUIC stream - go func() { - defer wg.Done() - if _, err := io.Copy(quicStream, tcpConn); err != nil { - log.Debug().Msgf("Error copying postgres->quic: %v", err) - } - // Close the write side of the QUIC stream - if err := quicStream.Close(); err != nil && !strings.Contains(err.Error(), "close called for canceled stream") { - log.Error().Msgf("Error closing QUIC stream write: %v", err) - } - }() - - // Wait for both copies to complete - wg.Wait() -} diff --git a/cli/packages/gateway/constants.go b/cli/packages/gateway/constants.go deleted file mode 100644 index aa260ed2e..000000000 --- a/cli/packages/gateway/constants.go +++ /dev/null @@ -1,17 +0,0 @@ -package gateway - -const ( - KUBERNETES_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST" - KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME = "KUBERNETES_SERVICE_PORT_HTTPS" - KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" - - INFISICAL_HTTP_PROXY_ACTION_HEADER = "x-infisical-action" -) - -type HttpProxyAction string - -const ( - HttpProxyActionInjectGatewayK8sServiceAccountToken HttpProxyAction = "inject-k8s-sa-auth-token" - HttpProxyActionUseGatewayK8sServiceAccount HttpProxyAction = "use-k8s-sa" -) diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go deleted file mode 100644 index eb0c72d5d..000000000 --- a/cli/packages/gateway/gateway.go +++ /dev/null @@ -1,371 +0,0 @@ -package gateway - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "net" - "os" - "strings" - "sync" - "time" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/systemd" - "github.com/Infisical/infisical-merge/packages/util" - "github.com/go-resty/resty/v2" - "github.com/pion/dtls/v3" - "github.com/pion/logging" - "github.com/pion/turn/v4" - "github.com/rs/zerolog/log" - - "github.com/quic-go/quic-go" -) - -type GatewayConfig struct { - TurnServerUsername string - TurnServerPassword string - TurnServerAddress string - InfisicalStaticIp string - SerialNumber string - PrivateKey string - Certificate string - CertificateChain string -} - -type Gateway struct { - httpClient *resty.Client - config *GatewayConfig - client *turn.Client -} - -func NewGateway(identityToken string) (Gateway, error) { - httpClient, err := util.GetRestyClientWithCustomHeaders() - if err != nil { - return Gateway{}, fmt.Errorf("unable to get client with custom headers [err=%v]", err) - } - - httpClient.SetAuthToken(identityToken) - - return Gateway{ - httpClient: httpClient, - config: &GatewayConfig{}, - }, nil -} - -func (g *Gateway) UpdateIdentityAccessToken(accessToken string) { - g.httpClient.SetAuthToken(accessToken) -} - -func (g *Gateway) ConnectWithRelay() error { - relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient) - if err != nil { - return err - } - relayAddress, relayPort := strings.Split(relayDetails.TurnServerAddress, ":")[0], strings.Split(relayDetails.TurnServerAddress, ":")[1] - - // Start a new TURN Client and wrap our net.Conn in a STUNConn - // This allows us to simulate datagram based communication over a net.Conn - logger := logging.NewDefaultLoggerFactory() - if os.Getenv("LOG_LEVEL") == "debug" { - logger.DefaultLogLevel = logging.LogLevelDebug - } - - turnClientCfg := &turn.ClientConfig{ - STUNServerAddr: relayDetails.TurnServerAddress, - TURNServerAddr: relayDetails.TurnServerAddress, - Username: relayDetails.TurnServerUsername, - Password: relayDetails.TurnServerPassword, - Realm: relayDetails.TurnServerRealm, - LoggerFactory: logger, - } - - turnAddr, err := net.ResolveUDPAddr("udp4", relayDetails.TurnServerAddress) - if err != nil { - return fmt.Errorf("Failed to parse turn server address: %w", err) - } - - // Dial TURN Server - if relayPort == "5349" { - log.Info().Msgf("Provided relay port %s. Using TLS", relayPort) - conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{ - ServerName: relayAddress, - }) - if err != nil { - return fmt.Errorf("Failed to connect with relay server: %w", err) - } - turnClientCfg.Conn = turn.NewSTUNConn(conn) - } else { - log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort) - conn, err := net.ListenPacket("udp4", "0.0.0.0:0") - if err != nil { - return fmt.Errorf("Failed to connect with relay server: %w", err) - } - - turnClientCfg.Conn = conn - } - - client, err := turn.NewClient(turnClientCfg) - if err != nil { - return fmt.Errorf("Failed to create relay client: %w", err) - } - - g.config = &GatewayConfig{ - TurnServerUsername: relayDetails.TurnServerUsername, - TurnServerPassword: relayDetails.TurnServerPassword, - TurnServerAddress: relayDetails.TurnServerAddress, - InfisicalStaticIp: relayDetails.InfisicalStaticIp, - } - - g.client = client - return nil -} - -func (g *Gateway) Listen(ctx context.Context) error { - defer g.client.Close() - err := g.client.Listen() - if err != nil { - return fmt.Errorf("Failed to listen to relay server: %w", err) - } - - log.Info().Msg("Connected with relay") - - // Allocate a relay socket on the TURN server. On success, it - // will return a net.PacketConn which represents the remote - // socket. - relayUdpConnection, err := g.client.Allocate() - if err != nil { - return fmt.Errorf("Failed to allocate relay connection: %w", err) - } - - log.Info().Msg(relayUdpConnection.LocalAddr().String()) - defer func() { - if closeErr := relayUdpConnection.Close(); closeErr != nil { - log.Error().Msgf("Failed to close connection: %s", closeErr) - } - }() - - gatewayCert, err := api.CallExchangeRelayCertV1(g.httpClient, api.ExchangeRelayCertRequestV1{ - RelayAddress: relayUdpConnection.LocalAddr().String(), - }) - if err != nil { - return err - } - - g.config.SerialNumber = gatewayCert.SerialNumber - g.config.PrivateKey = gatewayCert.PrivateKey - g.config.Certificate = gatewayCert.Certificate - g.config.CertificateChain = gatewayCert.CertificateChain - - errCh := make(chan error, 1) - shutdownCh := make(chan bool, 1) - - if err = g.createPermissionForStaticIps(g.config.InfisicalStaticIp); err != nil { - return err - } - - g.registerHeartBeat(ctx, errCh) - - cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey)) - if err != nil { - return fmt.Errorf("failed to parse cert: %w", err) - } - - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM([]byte(gatewayCert.CertificateChain)) - - // Setup QUIC server - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - MinVersion: tls.VersionTLS12, - ClientCAs: caCertPool, - ClientAuth: tls.RequireAndVerifyClientCert, - NextProtos: []string{"infisical-gateway"}, - } - // Setup QUIC listener on the relayConn - quicConfig := &quic.Config{ - EnableDatagrams: true, - MaxIdleTimeout: 10 * time.Second, - KeepAlivePeriod: 2 * time.Second, - } - - quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig) - if err != nil { - return fmt.Errorf("Failed to listen for QUIC: %w", err) - } - defer quicListener.Close() - - log.Printf("Listener started on %s", quicListener.Addr()) - - g.registerRelayIsActive(ctx, errCh) - - log.Info().Msg("Gateway started successfully") - - var wg sync.WaitGroup - - go func() { - for { - select { - case <-ctx.Done(): - return - case <-shutdownCh: - return - default: - // Accept new relay connection - quicConn, err := quicListener.Accept(context.Background()) - if err != nil { - log.Printf("Failed to accept QUIC connection: %v", err) - continue - } - - tlsState := quicConn.ConnectionState().TLS - if len(tlsState.PeerCertificates) > 0 { - organizationUnit := tlsState.PeerCertificates[0].Subject.OrganizationalUnit - commonName := tlsState.PeerCertificates[0].Subject.CommonName - if organizationUnit[0] != "gateway-client" || commonName != "cloud" { - errMsg := fmt.Sprintf("Client certificate verification failed. Received %s, %s", organizationUnit, commonName) - log.Error().Msg(errMsg) - quicConn.CloseWithError(1, errMsg) - continue - } - } - - // Handle the connection in a goroutine - wg.Add(1) - go func(c quic.Connection) { - defer wg.Done() - defer c.CloseWithError(0, "connection closed") - - // Monitor parent context to close this connection when needed - go func() { - select { - case <-ctx.Done(): - c.CloseWithError(0, "connection closed") // Force close connection when context is canceled - case <-shutdownCh: - c.CloseWithError(0, "connection closed") // Force close connection when accepting loop is done - } - }() - - handleConnection(ctx, c) - }(quicConn) - } - } - }() - - // make this compatiable with systemd notify mode - systemd.SdNotify(false, systemd.SdNotifyReady) - select { - case <-ctx.Done(): - log.Info().Msg("Shutting down gateway...") - case err = <-errCh: - log.Error().Err(err).Msg("Gateway error occurred") - } - - // Signal the accept loop to stop - close(shutdownCh) - - // Set a timeout for waiting on connections to close - waitCh := make(chan struct{}) - go func() { - wg.Wait() - close(waitCh) - }() - - select { - case <-waitCh: - // All connections closed normally - case <-time.After(5 * time.Second): - log.Warn().Msg("Timeout waiting for connections to close gracefully") - } - - return err -} - -func (g *Gateway) registerHeartBeat(ctx context.Context, errCh chan error) { - ticker := time.NewTicker(30 * time.Minute) - defer ticker.Stop() - - go func() { - for { - if err := api.CallGatewayHeartBeatV1(g.httpClient); err != nil { - errCh <- err - } else { - log.Info().Msg("Gateway is reachable by Infisical") - } - - select { - case <-ctx.Done(): - return - case <-ticker.C: - } - } - }() -} - -func (g *Gateway) createPermissionForStaticIps(staticIps string) error { - if staticIps == "" { - return fmt.Errorf("Missing Infisical static ips for permission") - } - - splittedIps := strings.Split(staticIps, ",") - resolvedIps := make([]net.Addr, 0) - for _, ip := range splittedIps { - ip = strings.TrimSpace(ip) - if ip == "" { - continue - } - - // if port not specific allow all port - if !strings.Contains(ip, ":") { - ip = ip + ":0" - } - - peerAddr, err := net.ResolveUDPAddr("udp", ip) - if err != nil { - return fmt.Errorf("Failed to resolve static ip for permission: %w", err) - } - - resolvedIps = append(resolvedIps, peerAddr) - } - - if err := g.client.CreatePermission(resolvedIps...); err != nil { - return fmt.Errorf("Failed to set ip permission: %w", err) - } - return nil -} - -func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) error { - ticker := time.NewTicker(15 * time.Second) - maxFailures := 3 - failures := 0 - - log.Info().Msg("Starting relay connection health check") - go func() { - time.Sleep(5 * time.Second) - for { - select { - case <-ctx.Done(): - log.Info().Msg("Stopping relay connection health check") - return - case <-ticker.C: - log.Debug().Msg("Performing relay connection health check") - err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp) - // try again error message from server happens to avoid congestion - // https://github.com/pion/turn/blob/master/internal/client/udp_conn.go#L382 - if err != nil && !strings.Contains(err.Error(), "try again") { - failures++ - log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions") - if failures >= maxFailures { - errCh <- fmt.Errorf("relay connection check failed: %w", err) - return - } - continue - } - failures = 0 // reset - } - } - }() - - return nil -} diff --git a/cli/packages/gateway/relay.go b/cli/packages/gateway/relay.go deleted file mode 100644 index 08a5eb247..000000000 --- a/cli/packages/gateway/relay.go +++ /dev/null @@ -1,188 +0,0 @@ -//go:build !windows -// +build !windows - -package gateway - -import ( - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "net" - "os" - "os/signal" - - // "runtime" - "strconv" - "syscall" - - "github.com/Infisical/infisical-merge/packages/systemd" - "github.com/pion/dtls/v3" - "github.com/pion/logging" - "github.com/pion/turn/v4" - "github.com/rs/zerolog/log" - "gopkg.in/yaml.v2" -) - -var ( - errMissingTlsCert = errors.New("Missing TLS files") -) - -type GatewayRelay struct { - Config *GatewayRelayConfig -} - -type GatewayRelayConfig struct { - PublicIP string `yaml:"public_ip"` - Port int `yaml:"port"` - Realm string `yaml:"realm"` - AuthSecret string `yaml:"auth_secret"` - RelayMinPort uint16 `yaml:"relay_min_port"` - RelayMaxPort uint16 `yaml:"relay_max_port"` - TlsCertPath string `yaml:"tls_cert_path"` - TlsPrivateKeyPath string `yaml:"tls_private_key_path"` - TlsCaPath string `yaml:"tls_ca_path"` - - tls tls.Certificate - tlsCa string - isTlsEnabled bool -} - -func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) { - cfgFile, err := os.ReadFile(configFilePath) - if err != nil { - return nil, err - } - var cfg GatewayRelayConfig - if err := yaml.Unmarshal(cfgFile, &cfg); err != nil { - return nil, err - } - - if cfg.PublicIP == "" { - return nil, fmt.Errorf("Missing public ip") - } - - if cfg.AuthSecret == "" { - return nil, fmt.Errorf("Missing auth secret") - } - - if cfg.Realm == "" { - cfg.Realm = "infisical.org" - } - - if cfg.RelayMinPort == 0 { - cfg.RelayMinPort = 49152 - } - - if cfg.RelayMaxPort == 0 { - cfg.RelayMaxPort = 65535 - } - - if cfg.Port == 0 { - cfg.Port = 3478 - } else if cfg.Port == 5349 { - if cfg.TlsCertPath == "" || cfg.TlsPrivateKeyPath == "" { - return nil, errMissingTlsCert - } - - cert, err := tls.LoadX509KeyPair(cfg.TlsCertPath, cfg.TlsPrivateKeyPath) - if err != nil { - return nil, fmt.Errorf("Failed to read load server tls key pair: %w", err) - } - - if cfg.TlsCaPath != "" { - ca, err := os.ReadFile(cfg.TlsCaPath) - if err != nil { - return nil, fmt.Errorf("Failed to read tls ca: %w", err) - } - cfg.tlsCa = string(ca) - } - - cfg.tls = cert - cfg.isTlsEnabled = true - } - - return &GatewayRelay{ - Config: &cfg, - }, nil -} - -func (g *GatewayRelay) Run() error { - addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:"+strconv.Itoa(g.Config.Port)) - if err != nil { - return fmt.Errorf("Failed to parse server address: %s", err) - } - - // NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages - // and process them yourself. - logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout) - - publicIP := g.Config.PublicIP - relayAddressGenerator := &turn.RelayAddressGeneratorPortRange{ - RelayAddress: net.ParseIP(publicIP), // Claim that we are listening on IP passed by user - Address: "0.0.0.0", // But actually be listening on every interface - MinPort: g.Config.RelayMinPort, - MaxPort: g.Config.RelayMaxPort, - } - - loggerF := logging.NewDefaultLoggerFactory() - loggerF.DefaultLogLevel = logging.LogLevelDebug - - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM([]byte(g.Config.tlsCa)) - - listenerConfigs := make([]turn.ListenerConfig, 0) - packetConfigs := make([]turn.PacketConnConfig, 0) - - if g.Config.isTlsEnabled { - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM([]byte(g.Config.tlsCa)) - dtlsServer, err := dtls.Listen("udp", addr, &dtls.Config{ - Certificates: []tls.Certificate{g.Config.tls}, - ClientCAs: caCertPool, - }) - if err != nil { - return fmt.Errorf("Failed to start dtls server: %w", err) - } - listenerConfigs = append(listenerConfigs, turn.ListenerConfig{ - RelayAddressGenerator: relayAddressGenerator, - Listener: dtlsServer, - }) - } else { - udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(g.Config.Port)) - if err != nil { - return fmt.Errorf("Failed to relay udp listener: %w", err) - } - packetConfigs = append(packetConfigs, turn.PacketConnConfig{ - RelayAddressGenerator: relayAddressGenerator, - PacketConn: udpListener, - }) - } - - server, err := turn.NewServer(turn.ServerConfig{ - Realm: g.Config.Realm, - AuthHandler: turn.LongTermTURNRESTAuthHandler(g.Config.AuthSecret, logger), - // PacketConnConfigs is a list of UDP Listeners and the configuration around them - ListenerConfigs: listenerConfigs, - PacketConnConfigs: packetConfigs, - LoggerFactory: loggerF, - }) - - if err != nil { - return fmt.Errorf("Failed to start server: %w", err) - } - - log.Info().Msgf("Relay listening on %d\n", g.Config.Port) - - // make this compatiable with systemd notify mode - systemd.SdNotify(false, systemd.SdNotifyReady) - // Block until user sends SIGINT or SIGTERM - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - <-sigs - - if err = server.Close(); err != nil { - return fmt.Errorf("Failed to close server: %w", err) - } - return nil -} diff --git a/cli/packages/gateway/relay_windows.go b/cli/packages/gateway/relay_windows.go deleted file mode 100644 index f3bf89bd0..000000000 --- a/cli/packages/gateway/relay_windows.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build windows -// +build windows - -package gateway - -import ( - "errors" -) - -var ( - errMissingTlsCert = errors.New("Missing TLS files") - errWindowsNotSupported = errors.New("Relay is not supported on Windows") -) - -type GatewayRelay struct { - Config *GatewayRelayConfig -} - -type GatewayRelayConfig struct { - PublicIP string - Port int - Realm string - AuthSecret string - RelayMinPort uint16 - RelayMaxPort uint16 - TlsCertPath string - TlsPrivateKeyPath string - TlsCaPath string -} - -func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) { - return nil, errWindowsNotSupported -} - -func (g *GatewayRelay) Run() error { - return errWindowsNotSupported -} diff --git a/cli/packages/gateway/systemd.go b/cli/packages/gateway/systemd.go deleted file mode 100644 index ac6663dff..000000000 --- a/cli/packages/gateway/systemd.go +++ /dev/null @@ -1,121 +0,0 @@ -package gateway - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - - "github.com/rs/zerolog/log" -) - -const systemdServiceTemplate = `[Unit] -Description=Infisical Gateway Service -After=network.target - -[Service] -Type=notify -NotifyAccess=all -EnvironmentFile=/etc/infisical/gateway.conf -ExecStart=infisical gateway -Restart=on-failure -InaccessibleDirectories=/home -PrivateTmp=yes -LimitCORE=infinity -LimitNOFILE=1000000 -LimitNPROC=60000 -LimitRTPRIO=infinity -LimitRTTIME=7000000 - -[Install] -WantedBy=multi-user.target -` - -func InstallGatewaySystemdService(token string, domain string) error { - if runtime.GOOS != "linux" { - log.Info().Msg("Skipping systemd service installation - not on Linux") - return nil - } - - if os.Geteuid() != 0 { - log.Info().Msg("Skipping systemd service installation - not running as root/sudo") - return nil - } - - configDir := "/etc/infisical" - if err := os.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("failed to create config directory: %v", err) - } - - configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token) - if domain != "" { - configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain) - } - - configPath := filepath.Join(configDir, "gateway.conf") - if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { - return fmt.Errorf("failed to write config file: %v", err) - } - - servicePath := "/etc/systemd/system/infisical-gateway.service" - if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil { - return fmt.Errorf("failed to write systemd service file: %v", err) - } - - reloadCmd := exec.Command("systemctl", "daemon-reload") - if err := reloadCmd.Run(); err != nil { - return fmt.Errorf("failed to reload systemd: %v", err) - } - - log.Info().Msg("Successfully installed systemd service") - log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway") - log.Info().Msg("To enable the service on boot, run: sudo systemctl enable infisical-gateway") - - return nil -} - -func UninstallGatewaySystemdService() error { - if runtime.GOOS != "linux" { - log.Info().Msg("Skipping systemd service uninstallation - not on Linux") - return nil - } - - if os.Geteuid() != 0 { - log.Info().Msg("Skipping systemd service uninstallation - not running as root/sudo") - return nil - } - - // Stop the service if it's running - stopCmd := exec.Command("systemctl", "stop", "infisical-gateway") - if err := stopCmd.Run(); err != nil { - log.Warn().Msgf("Failed to stop service: %v", err) - } - - // Disable the service - disableCmd := exec.Command("systemctl", "disable", "infisical-gateway") - if err := disableCmd.Run(); err != nil { - log.Warn().Msgf("Failed to disable service: %v", err) - } - - // Remove the service file - servicePath := "/etc/systemd/system/infisical-gateway.service" - if err := os.Remove(servicePath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove systemd service file: %v", err) - } - - // Remove the configuration file - configPath := "/etc/infisical/gateway.conf" - if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove config file: %v", err) - } - - // Reload systemd to apply changes - reloadCmd := exec.Command("systemctl", "daemon-reload") - if err := reloadCmd.Run(); err != nil { - return fmt.Errorf("failed to reload systemd: %v", err) - } - - log.Info().Msg("Successfully uninstalled Infisical Gateway systemd service") - return nil -} diff --git a/cli/packages/gateway/udp_listener/listener_unix.go b/cli/packages/gateway/udp_listener/listener_unix.go deleted file mode 100644 index 8de2828b4..000000000 --- a/cli/packages/gateway/udp_listener/listener_unix.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !windows -// +build !windows - -package udplistener - -import ( - "net" - "syscall" - - "golang.org/x/sys/unix" - // other imports -) - -func SetupListenerConfig() *net.ListenConfig { - return &net.ListenConfig{ - Control: func(network, address string, conn syscall.RawConn) error { - var operr error - if err := conn.Control(func(fd uintptr) { - operr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) - }); err != nil { - return err - } - return operr - }, - } -} diff --git a/cli/packages/gateway/udp_listener/listener_windows.go b/cli/packages/gateway/udp_listener/listener_windows.go deleted file mode 100644 index 4904d12e0..000000000 --- a/cli/packages/gateway/udp_listener/listener_windows.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build windows -// +build windows - -package udplistener - -import ( - "fmt" - "net" - "syscall" -) - -func SetupListenerConfig() *net.ListenConfig { - return &net.ListenConfig{ - Control: func(network, address string, conn syscall.RawConn) error { - return fmt.Errorf("Infisical relay not supported for windows.") - }, - } -} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go deleted file mode 100644 index 8b9fef6f6..000000000 --- a/cli/packages/models/cli.go +++ /dev/null @@ -1,161 +0,0 @@ -package models - -import "time" - -type UserCredentials struct { - Email string `json:"email"` - PrivateKey string `json:"privateKey"` - JTWToken string `json:"JTWToken"` - RefreshToken string `json:"RefreshToken"` -} - -// The file struct for Infisical config file -type ConfigFile struct { - LoggedInUserEmail string `json:"loggedInUserEmail"` - LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"` - LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"` - VaultBackendType string `json:"vaultBackendType,omitempty"` - VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"` - Domains []string `json:"domains,omitempty"` -} - -type LoggedInUser struct { - Email string `json:"email"` - Domain string `json:"domain"` -} - -type SingleEnvironmentVariable struct { - Key string `json:"key"` - WorkspaceId string `json:"workspace"` - Value string `json:"value"` - Type string `json:"type"` - ID string `json:"_id"` - SecretPath string `json:"secretPath"` - Tags []struct { - ID string `json:"_id"` - Name string `json:"name"` - Slug string `json:"slug"` - Workspace string `json:"workspace"` - } `json:"tags"` - Comment string `json:"comment"` - Etag string `json:"Etag"` -} - -type PlaintextSecretResult struct { - Secrets []SingleEnvironmentVariable - Etag string -} - -type DynamicSecret struct { - Id string `json:"id"` - DefaultTTL string `json:"defaultTTL"` - MaxTTL string `json:"maxTTL"` - Type string `json:"type"` -} - -type DynamicSecretLease struct { - Lease struct { - Id string `json:"id"` - ExpireAt time.Time `json:"expireAt"` - } `json:"lease"` - DynamicSecret DynamicSecret `json:"dynamicSecret"` - // this is a varying dict based on provider - Data map[string]interface{} `json:"data"` -} - -type TokenDetails struct { - Type string - Token string - Source string -} - -type SingleFolder struct { - ID string `json:"_id"` - Name string `json:"name"` -} - -type Workspace struct { - ID string `json:"_id"` - Name string `json:"name"` - Plan string `json:"plan,omitempty"` - V int `json:"__v"` - OrganizationId string `json:"orgId"` -} - -type WorkspaceConfigFile struct { - WorkspaceId string `json:"workspaceId"` - DefaultEnvironment string `json:"defaultEnvironment"` - GitBranchToEnvironmentMapping map[string]string `json:"gitBranchToEnvironmentMapping"` -} - -type SymmetricEncryptionResult struct { - CipherText []byte `json:"CipherText"` - Nonce []byte `json:"Nonce"` - AuthTag []byte `json:"AuthTag"` -} - -type GetAllSecretsParameters struct { - Environment string - EnvironmentPassedViaFlag bool - InfisicalToken string - UniversalAuthAccessToken string - TagSlugs string - WorkspaceId string - SecretsPath string - IncludeImport bool - Recursive bool - ExpandSecretReferences bool -} - -type InjectableEnvironmentResult struct { - Variables []string - ETag string - SecretsCount int -} - -type GetAllFoldersParameters struct { - WorkspaceId string - Environment string - FoldersPath string - InfisicalToken string - UniversalAuthAccessToken string -} - -type CreateFolderParameters struct { - FolderName string - WorkspaceId string - Environment string - FolderPath string - InfisicalToken string -} - -type DeleteFolderParameters struct { - FolderName string - WorkspaceId string - Environment string - FolderPath string - InfisicalToken string -} - -type ExpandSecretsAuthentication struct { - InfisicalToken string - UniversalAuthAccessToken string -} - -type MachineIdentityCredentials struct { - ClientId string - ClientSecret string -} - -type SecretSetOperation struct { - SecretKey string - SecretValue string - SecretOperation string -} - -type BackupSecretKeyRing struct { - ProjectID string `json:"projectId"` - Environment string `json:"environment"` - SecretPath string `json:"secretPath"` - Secrets []SingleEnvironmentVariable -} diff --git a/cli/packages/srp/client.go b/cli/packages/srp/client.go deleted file mode 100644 index 823f8f6fa..000000000 --- a/cli/packages/srp/client.go +++ /dev/null @@ -1,140 +0,0 @@ -package srp - -import ( - "bytes" - "errors" - "math/big" -) - -type SRPClient struct { - Params *SRPParams - Secret1 *big.Int - Multiplier *big.Int - A *big.Int - X *big.Int - M1 []byte - M2 []byte - K []byte - u *big.Int - s *big.Int -} - -func NewClient(params *SRPParams, identity, password, secret1 []byte) *SRPClient { - multiplier := getMultiplier(params) - secret1Int := intFromBytes(secret1) - Ab := getA(params, secret1Int) - A := intFromBytes(Ab) - x := getx(params, []byte(""), identity, password) // salt has to be set using SetSalt - - return &SRPClient{ - Params: params, - Multiplier: multiplier, - Secret1: secret1Int, - A: A, - X: x, - } -} - -func (c *SRPClient) ComputeA() []byte { - return intToBytes(c.A) -} - -// ComputeVerifier returns a verifier that is calculated as described in -// Section 3 of [SRP-RFC] -func ComputeVerifier(params *SRPParams, salt, identity, password []byte) []byte { - x := getx(params, salt, identity, password) - vNum := new(big.Int) - vNum.Exp(params.G, x, params.N) - - return padToN(vNum, params) -} - -func (c *SRPClient) SetB(Bb []byte) { - B := intFromBytes(Bb) - u := getu(c.Params, c.A, B) - S := clientGetS(c.Params, c.Multiplier, c.X, c.Secret1, B, u) - - c.K = getK(c.Params, S) - c.M1 = getM1(c.Params, intToBytes(c.A), Bb, c.K) // modified S -> c.K - c.M2 = getM2(c.Params, intToBytes(c.A), c.M1, c.K) - - c.u = u // Only for tests - c.s = intFromBytes(S) // Only for tests -} - -func (c *SRPClient) SetSalt(salt, identity, password []byte) { - c.X = getx(c.Params, salt, identity, password) //Overwrite -} - -func (c *SRPClient) ComputeM1() []byte { - if c.M1 == nil { - panic("Incomplete protocol") - } - - return c.M1 -} - -func (c *SRPClient) ComputeK() []byte { - return c.K -} - -func (c *SRPClient) CheckM2(M2 []byte) error { - if !bytes.Equal(c.M2, M2) { - return errors.New("M2 didn't check") - } else { - return nil - } -} - -func getA(params *SRPParams, a *big.Int) []byte { - ANum := new(big.Int) - ANum.Exp(params.G, a, params.N) - return padToN(ANum, params) -} - -func clientGetS(params *SRPParams, k, x, a, B, u *big.Int) []byte { - BLessThan0 := B.Cmp(big.NewInt(0)) <= 0 - NLessThanB := params.N.Cmp(B) <= 0 - if BLessThan0 || NLessThanB { - panic("invalid server-supplied 'B', must be 1..N-1") - } - - result1 := new(big.Int) - result1.Exp(params.G, x, params.N) - - result2 := new(big.Int) - result2.Mul(k, result1) - - result3 := new(big.Int) - result3.Sub(B, result2) - - result4 := new(big.Int) - result4.Mul(u, x) - - result5 := new(big.Int) - result5.Add(a, result4) - - result6 := new(big.Int) - result6.Exp(result3, result5, params.N) - - result7 := new(big.Int) - result7.Mod(result6, params.N) - - return padToN(result7, params) -} - -func getx(params *SRPParams, salt, I, P []byte) *big.Int { - var ipBytes []byte - ipBytes = append(ipBytes, I...) - ipBytes = append(ipBytes, []byte(":")...) - ipBytes = append(ipBytes, P...) - - hashIP := params.Hash.New() - hashIP.Write(ipBytes) - - hashX := params.Hash.New() - hashX.Write(salt) - hashX.Write(hashToBytes(hashIP)) - - return hashToInt(hashX) -} diff --git a/cli/packages/srp/params.go b/cli/packages/srp/params.go deleted file mode 100644 index 0fca25302..000000000 --- a/cli/packages/srp/params.go +++ /dev/null @@ -1,95 +0,0 @@ -package srp - -import ( - "crypto" - "fmt" - "math/big" -) - -// Map of bits to tuple -type SRPParams struct { - G *big.Int - N *big.Int - Hash crypto.Hash - NLengthBits int -} - -var knownGroups map[int]*SRPParams - -func createParams(G int64, nBitLength int, hash crypto.Hash, NHex string) *SRPParams { - p := SRPParams{ - G: big.NewInt(G), - N: new(big.Int), - NLengthBits: nBitLength, - Hash: hash, - } - - b := bytesFromHexString(NHex) - p.N.SetBytes(b) - return &p -} - -func GetParams(G int) *SRPParams { - params := knownGroups[G] - if params == nil { - panic(fmt.Sprintf("Params don't exist for %v", G)) - } else { - return params - } -} - -func init() { - knownGroups = make(map[int]*SRPParams) - - knownGroups[1024] = createParams(2, 1024, crypto.SHA1, ` - EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C - 9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4 - 8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29 - 7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A - FD5138FE 8376435B 9FC61D2F C0EB06E3`) - - knownGroups[1536] = createParams(2, 1536, crypto.SHA1, ` - 9DEF3CAF B939277A B1F12A86 17A47BBB DBA51DF4 99AC4C80 BEEEA961 - 4B19CC4D 5F4F5F55 6E27CBDE 51C6A94B E4607A29 1558903B A0D0F843 - 80B655BB 9A22E8DC DF028A7C EC67F0D0 8134B1C8 B9798914 9B609E0B - E3BAB63D 47548381 DBC5B1FC 764E3F4B 53DD9DA1 158BFD3E 2B9C8CF5 - 6EDF0195 39349627 DB2FD53D 24B7C486 65772E43 7D6C7F8C E442734A - F7CCB7AE 837C264A E3A9BEB8 7F8A2FE9 B8B5292E 5A021FFF 5E91479E - 8CE7A28C 2442C6F3 15180F93 499A234D CF76E3FE D135F9BB - `) - - knownGroups[2048] = createParams(2, 2048, crypto.SHA256, ` - AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294 - 3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D - CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB - D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74 - 7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A - 436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D - 5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73 - 03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6 - 94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F - 9E4AFF73 - `) - - knownGroups[4096] = createParams(5, 4096, crypto.SHA256, ` - FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 - 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B - 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 - A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 - 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 - FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D - 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C - 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 - 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D - 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D - B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 - 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C - BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC - E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 - 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB - 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 - 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 - D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199 - FFFFFFFF FFFFFFFF - `) -} diff --git a/cli/packages/srp/server.go b/cli/packages/srp/server.go deleted file mode 100644 index b8cdbe84d..000000000 --- a/cli/packages/srp/server.go +++ /dev/null @@ -1,104 +0,0 @@ -package srp - -import ( - "bytes" - "errors" - "math/big" -) - -type SRPServer struct { - Params *SRPParams - Verifier *big.Int - Secret2 *big.Int - B *big.Int - M1 []byte - M2 []byte - K []byte - u *big.Int - s *big.Int -} - -func NewServer(params *SRPParams, Vb []byte, S2b []byte) *SRPServer { - multiplier := getMultiplier(params) - V := intFromBytes(Vb) - secret2 := intFromBytes(S2b) - - Bb := getB(params, multiplier, V, secret2) - B := intFromBytes(Bb) - - return &SRPServer{ - Params: params, - Secret2: secret2, - Verifier: V, - B: B, - } -} - -func (s *SRPServer) ComputeB() []byte { - return intToBytes(s.B) -} - -func (s *SRPServer) SetA(A []byte) { - AInt := intFromBytes(A) - U := getu(s.Params, AInt, s.B) - S := serverGetS(s.Params, s.Verifier, AInt, s.Secret2, U) - - s.K = getK(s.Params, S) - s.M1 = getM1(s.Params, A, intToBytes(s.B), S) - s.M2 = getM2(s.Params, A, s.M1, s.K) - - s.u = U // only for tests - s.s = intFromBytes(S) // only for tests -} - -func (s *SRPServer) CheckM1(M1 []byte) ([]byte, error) { - if !bytes.Equal(s.M1, M1) { - return nil, errors.New("Client did not use the same password") - } else { - return s.M2, nil - } -} - -func (s *SRPServer) ComputeK() []byte { - return s.K -} - -// Helpers - -func serverGetS(params *SRPParams, V, A, S2, U *big.Int) []byte { - ALessThan0 := A.Cmp(big.NewInt(0)) <= 0 - NLessThanA := params.N.Cmp(A) <= 0 - if ALessThan0 || NLessThanA { - panic("invalid client-supplied 'A', must be 1..N-1") - } - - result1 := new(big.Int) - result1.Exp(V, U, params.N) - - result2 := new(big.Int) - result2.Mul(A, result1) - - result3 := new(big.Int) - result3.Exp(result2, S2, params.N) - - result4 := new(big.Int) - result4.Mod(result3, params.N) - - return padToN(result4, params) -} - -func getB(params *SRPParams, multiplier, V, b *big.Int) []byte { - gModPowB := new(big.Int) - gModPowB.Exp(params.G, b, params.N) - - kMulV := new(big.Int) - kMulV.Mul(multiplier, V) - - leftSide := new(big.Int) - leftSide.Add(kMulV, gModPowB) - - final := new(big.Int) - final.Mod(leftSide, params.N) - - return padToN(final, params) -} diff --git a/cli/packages/srp/srp.go b/cli/packages/srp/srp.go deleted file mode 100644 index 448a94d1a..000000000 --- a/cli/packages/srp/srp.go +++ /dev/null @@ -1,103 +0,0 @@ -// Package srp is port of node-srp to Go. -// -// To use SRP, first decide on they parameters you will use. Both client and server must -// use the same set. -// -// params := srp.GetParams(4096) -// -// From the client... generate a new secret key, initialize the client, and compute A. -// Once you have A, you can send A to the server. -// -// secret1 := srp.GenKey() -// client := NewClient(params, salt, identity, secret, a) -// srpA := client.computeA() -// -// sendToServer(srpA) -// -// From the server... generate another secret key, initialize the server, and compute B. -// Once you have B, you can send B to the client. -// -// secret2 := srp.GenKey() -// server := NewServer(params, verifier, secret2) -// srpB := client.computeB() -// -// sendToClient(srpB) -// -// Once the client received B from the server, it can compute M1 based on A and B. -// Once you have M1, send M1 to the server. -// -// client.setB(srpB) -// srpM1 := client.ComputeM1() -// sendM1ToServer(srpM1) -// -// Once the server receives M1, it can verify that it is correct. If checkM1() returns -// an error, authentication failed. If it succeeds it should be sent to the client. -// -// srpM2, err := server.checkM1(srpM1) -// -// Once the client receives M2, it can verify that it is correct, and know that authentication -// was successful. -// -// err = client.CheckM2(serverM2) -// -// Now that both client and server have completed a successful authentication, they can -// both compute K independently. K can now be used as either a key to encrypt communication -// or as a session ID. -// -// clientK := client.ComputeK() -// serverK := server.ComputeK() -package srp - -import ( - "crypto/rand" - "io" - "math/big" -) - -func GenKey() []byte { - bytes := make([]byte, 32) - _, err := io.ReadFull(rand.Reader, bytes) - if err != nil { - panic("Random source is broken!") - } - - return bytes -} - -func getK(params *SRPParams, S []byte) []byte { - hashK := params.Hash.New() - hashK.Write(S) - return hashToBytes(hashK) -} - -func getu(params *SRPParams, A, B *big.Int) *big.Int { - hashU := params.Hash.New() - hashU.Write(A.Bytes()) - hashU.Write(B.Bytes()) - - return hashToInt(hashU) -} - -func getM1(params *SRPParams, A, B, S []byte) []byte { - hashM1 := params.Hash.New() - hashM1.Write(A) - hashM1.Write(B) - hashM1.Write(S) - return hashToBytes(hashM1) -} - -func getM2(params *SRPParams, A, M, K []byte) []byte { - hashM1 := params.Hash.New() - hashM1.Write(A) - hashM1.Write(M) - hashM1.Write(K) - return hashToBytes(hashM1) -} - -func getMultiplier(params *SRPParams) *big.Int { - hashK := params.Hash.New() - hashK.Write(padToN(params.N, params)) - hashK.Write(padToN(params.G, params)) - - return hashToInt(hashK) -} diff --git a/cli/packages/srp/util.go b/cli/packages/srp/util.go deleted file mode 100644 index 60929bfd5..000000000 --- a/cli/packages/srp/util.go +++ /dev/null @@ -1,48 +0,0 @@ -package srp - -import ( - "encoding/hex" - "hash" - "math/big" - "regexp" -) - -// Helpers - -func padTo(bytes []byte, length int) []byte { - paddingLength := length - len(bytes) - padding := make([]byte, paddingLength, paddingLength) - - return append(padding, bytes...) -} - -func padToN(number *big.Int, params *SRPParams) []byte { - return padTo(number.Bytes(), params.NLengthBits/8) -} - -func hashToBytes(h hash.Hash) []byte { - return h.Sum(nil) -} - -func hashToInt(h hash.Hash) *big.Int { - U := new(big.Int) - U.SetBytes(hashToBytes(h)) - return U -} - -func intFromBytes(bytes []byte) *big.Int { - i := new(big.Int) - i.SetBytes(bytes) - return i -} - -func intToBytes(i *big.Int) []byte { - return i.Bytes() -} - -func bytesFromHexString(s string) []byte { - re, _ := regexp.Compile("[^0-9a-fA-F]") - h := re.ReplaceAll([]byte(s), []byte("")) - b, _ := hex.DecodeString(string(h)) - return b -} diff --git a/cli/packages/systemd/daemon.go b/cli/packages/systemd/daemon.go deleted file mode 100644 index ce3c97394..000000000 --- a/cli/packages/systemd/daemon.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2014 Docker, Inc. -// Copyright 2015-2018 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -// Package daemon provides a Go implementation of the sd_notify protocol. -// It can be used to inform systemd of service start-up completion, watchdog -// events, and other status changes. -// -// https://www.freedesktop.org/software/systemd/man/sd_notify.html#Description -package systemd - -import ( - "net" - "os" -) - -const ( - // SdNotifyReady tells the service manager that service startup is finished - // or the service finished loading its configuration. - SdNotifyReady = "READY=1" - - // SdNotifyStopping tells the service manager that the service is beginning - // its shutdown. - SdNotifyStopping = "STOPPING=1" - - // SdNotifyReloading tells the service manager that this service is - // reloading its configuration. Note that you must call SdNotifyReady when - // it completed reloading. - SdNotifyReloading = "RELOADING=1" - - // SdNotifyWatchdog tells the service manager to update the watchdog - // timestamp for the service. - SdNotifyWatchdog = "WATCHDOG=1" -) - -// SdNotify sends a message to the init daemon. It is common to ignore the error. -// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET` -// will be unconditionally unset. -// -// It returns one of the following: -// (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset) -// (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data) -// (true, nil) - notification supported, data has been sent -func SdNotify(unsetEnvironment bool, state string) (bool, error) { - socketAddr := &net.UnixAddr{ - Name: os.Getenv("NOTIFY_SOCKET"), - Net: "unixgram", - } - - // NOTIFY_SOCKET not set - if socketAddr.Name == "" { - return false, nil - } - - if unsetEnvironment { - if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil { - return false, err - } - } - - conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) - // Error connecting to NOTIFY_SOCKET - if err != nil { - return false, err - } - defer conn.Close() - - if _, err = conn.Write([]byte(state)); err != nil { - return false, err - } - return true, nil -} diff --git a/cli/packages/telemetry/telemetry.go b/cli/packages/telemetry/telemetry.go deleted file mode 100644 index ffd743457..000000000 --- a/cli/packages/telemetry/telemetry.go +++ /dev/null @@ -1,82 +0,0 @@ -package telemetry - -import ( - "github.com/Infisical/infisical-merge/packages/util" - "github.com/denisbrodbeck/machineid" - "github.com/posthog/posthog-go" - "github.com/rs/zerolog/log" -) - -var POSTHOG_API_KEY_FOR_CLI string - -type Telemetry struct { - isEnabled bool - posthogClient posthog.Client -} - -type NoOpLogger struct{} - -func (NoOpLogger) Logf(format string, args ...interface{}) { - log.Debug().Msgf(format, args...) -} - -func (NoOpLogger) Errorf(format string, args ...interface{}) { - log.Debug().Msgf(format, args...) -} - -func NewTelemetry(telemetryIsEnabled bool) *Telemetry { - if POSTHOG_API_KEY_FOR_CLI != "" { - client, _ := posthog.NewWithConfig( - POSTHOG_API_KEY_FOR_CLI, - posthog.Config{ - Logger: NoOpLogger{}, - }, - ) - - return &Telemetry{isEnabled: telemetryIsEnabled, posthogClient: client} - } else { - return &Telemetry{isEnabled: false} - } -} - -func (t *Telemetry) CaptureEvent(eventName string, properties posthog.Properties) { - userIdentity, err := t.GetDistinctId() - if err != nil { - return - } - - if t.isEnabled { - t.posthogClient.Enqueue(posthog.Capture{ - DistinctId: userIdentity, - Event: eventName, - Properties: properties, - }) - - defer t.posthogClient.Close() - } -} - -func (t *Telemetry) GetDistinctId() (string, error) { - var distinctId string - var outputErr error - - machineId, err := machineid.ID() - if err != nil { - outputErr = err - } - - infisicalConfig, err := util.GetConfigFile() - if err != nil { - outputErr = err - } - - if infisicalConfig.LoggedInUserEmail != "" { - distinctId = infisicalConfig.LoggedInUserEmail - } else if machineId != "" { - distinctId = "anonymous_cli_" + machineId - } else { - distinctId = "" - } - - return distinctId, outputErr -} diff --git a/cli/packages/util/agent.go b/cli/packages/util/agent.go deleted file mode 100644 index 215e43551..000000000 --- a/cli/packages/util/agent.go +++ /dev/null @@ -1,41 +0,0 @@ -package util - -import ( - "fmt" - "strconv" - "time" -) - -// ConvertPollingIntervalToTime converts a string representation of a polling interval to a time.Duration -func ConvertPollingIntervalToTime(pollingInterval string) (time.Duration, error) { - length := len(pollingInterval) - if length < 2 { - return 0, fmt.Errorf("invalid format") - } - - unit := pollingInterval[length-1:] - numberPart := pollingInterval[:length-1] - - number, err := strconv.Atoi(numberPart) - if err != nil { - return 0, err - } - - switch unit { - case "s": - if number < 60 { - return 0, fmt.Errorf("polling interval must be at least 60 seconds") - } - return time.Duration(number) * time.Second, nil - case "m": - return time.Duration(number) * time.Minute, nil - case "h": - return time.Duration(number) * time.Hour, nil - case "d": - return time.Duration(number) * 24 * time.Hour, nil - case "w": - return time.Duration(number) * 7 * 24 * time.Hour, nil - default: - return 0, fmt.Errorf("invalid time unit") - } -} diff --git a/cli/packages/util/auth.go b/cli/packages/util/auth.go deleted file mode 100644 index eaf7cecc1..000000000 --- a/cli/packages/util/auth.go +++ /dev/null @@ -1,208 +0,0 @@ -package util - -import ( - "fmt" - "os" - "os/exec" - - infisicalSdk "github.com/infisical/go-sdk" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -type AuthStrategyType string - -var AuthStrategy = struct { - UNIVERSAL_AUTH AuthStrategyType - KUBERNETES_AUTH AuthStrategyType - AZURE_AUTH AuthStrategyType - GCP_ID_TOKEN_AUTH AuthStrategyType - GCP_IAM_AUTH AuthStrategyType - AWS_IAM_AUTH AuthStrategyType - OIDC_AUTH AuthStrategyType - JWT_AUTH AuthStrategyType -}{ - UNIVERSAL_AUTH: "universal-auth", - KUBERNETES_AUTH: "kubernetes", - AZURE_AUTH: "azure", - GCP_ID_TOKEN_AUTH: "gcp-id-token", - GCP_IAM_AUTH: "gcp-iam", - AWS_IAM_AUTH: "aws-iam", - OIDC_AUTH: "oidc-auth", - JWT_AUTH: "jwt-auth", -} - -var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{ - AuthStrategy.UNIVERSAL_AUTH, - AuthStrategy.KUBERNETES_AUTH, - AuthStrategy.AZURE_AUTH, - AuthStrategy.GCP_ID_TOKEN_AUTH, - AuthStrategy.GCP_IAM_AUTH, - AuthStrategy.AWS_IAM_AUTH, - AuthStrategy.OIDC_AUTH, - AuthStrategy.JWT_AUTH, -} - -func IsAuthMethodValid(authMethod string, allowUserAuth bool) (isValid bool, strategy AuthStrategyType) { - - if authMethod == "user" && allowUserAuth { - return true, "" - } - - for _, strategy := range AVAILABLE_AUTH_STRATEGIES { - if string(strategy) == authMethod { - return true, strategy - } - } - return false, "" -} - -// EstablishUserLoginSession handles the login flow to either create a new session or restore an expired one. -// It returns fresh user details if login is successful. -func EstablishUserLoginSession() LoggedInUserDetails { - log.Info().Msg("No valid login session found, triggering login flow") - - exePath, err := os.Executable() - if err != nil { - PrintErrorMessageAndExit(fmt.Sprintf("Failed to determine executable path: %v", err)) - } - - // Spawn infisical login command - loginCmd := exec.Command(exePath, "login", "--silent") - loginCmd.Stdin = os.Stdin - loginCmd.Stdout = os.Stdout - loginCmd.Stderr = os.Stderr - - err = loginCmd.Run() - if err != nil { - PrintErrorMessageAndExit(fmt.Sprintf("Failed to automatically trigger login flow. Please run [infisical login] manually to login.")) - } - - loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) - if err != nil { - PrintErrorMessageAndExit("You must be logged in to run this command. To login, run [infisical login]") - } - - if loggedInUserDetails.LoginExpired { - PrintErrorMessageAndExit("Your login session has expired. Please run [infisical login]") - } - - return loggedInUserDetails -} - -type SdkAuthenticator struct { - infisicalClient infisicalSdk.InfisicalClientInterface - cmd *cobra.Command -} - -func NewSdkAuthenticator(infisicalClient infisicalSdk.InfisicalClientInterface, cmd *cobra.Command) *SdkAuthenticator { - return &SdkAuthenticator{ - infisicalClient: infisicalClient, - cmd: cmd, - } -} -func (a *SdkAuthenticator) HandleUniversalAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { - - clientId, err := GetCmdFlagOrEnv(a.cmd, "client-id", []string{INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME}) - - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - clientSecret, err := GetCmdFlagOrEnv(a.cmd, "client-secret", []string{INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return a.infisicalClient.Auth().UniversalAuthLogin(clientId, clientSecret) -} - -func (a *SdkAuthenticator) HandleJwtAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - jwt, err := GetCmdFlagOrEnv(a.cmd, "jwt", []string{INFISICAL_JWT_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return a.infisicalClient.Auth().JwtAuthLogin(identityId, jwt) -} - -func (a *SdkAuthenticator) HandleKubernetesAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - serviceAccountTokenPath, err := GetCmdFlagOrEnv(a.cmd, "service-account-token-path", []string{INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return a.infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath) -} - -func (a *SdkAuthenticator) HandleAzureAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return a.infisicalClient.Auth().AzureAuthLogin(identityId, "") -} - -func (a *SdkAuthenticator) HandleGcpIdTokenAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return a.infisicalClient.Auth().GcpIdTokenAuthLogin(identityId) -} - -func (a *SdkAuthenticator) HandleGcpIamAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - serviceAccountKeyFilePath, err := GetCmdFlagOrEnv(a.cmd, "service-account-key-file-path", []string{INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return a.infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyFilePath) -} - -func (a *SdkAuthenticator) HandleAwsIamAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return a.infisicalClient.Auth().AwsIamAuthLogin(identityId) -} - -func (a *SdkAuthenticator) HandleOidcAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) { - - identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - jwt, err := GetCmdFlagOrEnv(a.cmd, "jwt", []string{INFISICAL_JWT_NAME, INFISICAL_OIDC_AUTH_JWT_NAME}) - if err != nil { - return infisicalSdk.MachineIdentityCredential{}, err - } - - return a.infisicalClient.Auth().OidcAuthLogin(identityId, jwt) -} diff --git a/cli/packages/util/check-for-update.go b/cli/packages/util/check-for-update.go deleted file mode 100644 index a1e35f656..000000000 --- a/cli/packages/util/check-for-update.go +++ /dev/null @@ -1,183 +0,0 @@ -package util - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "os/exec" - "runtime" - "strings" - - "github.com/fatih/color" - "github.com/rs/zerolog/log" -) - -func CheckForUpdate() { - if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" { - return - } - latestVersion, _, err := getLatestTag("Infisical", "cli") - if err != nil { - log.Debug().Err(err) - // do nothing and continue - return - } - - // daysSinceRelease, _ := daysSinceDate(publishedDate) - - if latestVersion != CLI_VERSION { - yellow := color.New(color.FgYellow).SprintFunc() - blue := color.New(color.FgCyan).SprintFunc() - black := color.New(color.FgBlack).SprintFunc() - - msg := fmt.Sprintf("%s %s %s %s", - yellow("A new release of infisical is available:"), - blue(CLI_VERSION), - black("->"), - blue(latestVersion), - ) - - fmt.Fprintln(os.Stderr, msg) - - updateInstructions := GetUpdateInstructions() - - if updateInstructions != "" { - msg = fmt.Sprintf("\n%s\n", GetUpdateInstructions()) - fmt.Fprintln(os.Stderr, msg) - } - - } -} - -func DisplayAptInstallationChangeBanner(isSilent bool) { - if isSilent { - return - } - - if runtime.GOOS == "linux" { - _, err := exec.LookPath("apt-get") - isApt := err == nil - if isApt { - yellow := color.New(color.FgYellow).SprintFunc() - msg := fmt.Sprintf("%s", - yellow("Update Required: Your current package installation script is outdated and will no longer receive updates.\nPlease update to the new installation script which can be found here https://infisical.com/docs/cli/overview#installation debian section\n"), - ) - - fmt.Fprintln(os.Stderr, msg) - } - } -} - -func getLatestTag(repoOwner string, repoName string) (string, string, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName) - resp, err := http.Get(url) - if err != nil { - return "", "", err - } - if resp.StatusCode != 200 { - return "", "", errors.New(fmt.Sprintf("gitHub API returned status code %d", resp.StatusCode)) - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", "", err - } - - var releaseDetails struct { - TagName string `json:"tag_name"` - PublishedAt string `json:"published_at"` - } - - if err := json.Unmarshal(body, &releaseDetails); err != nil { - return "", "", fmt.Errorf("failed to unmarshal github response: %w", err) - } - - tag_prefix := "v" - - // Extract the version from the first valid tag - version := strings.TrimPrefix(releaseDetails.TagName, tag_prefix) - - return version, releaseDetails.PublishedAt, nil -} - -func GetUpdateInstructions() string { - os := runtime.GOOS - switch os { - case "darwin": - return "To update, run: brew update && brew upgrade infisical" - case "windows": - return "To update, run: scoop update infisical" - case "linux": - pkgManager := getLinuxPackageManager() - switch pkgManager { - case "apt-get": - return "To update, run: sudo apt-get update && sudo apt-get install infisical" - case "yum": - return "To update, run: sudo yum update infisical" - case "apk": - return "To update, run: sudo apk update && sudo apk upgrade infisical" - case "yay": - return "To update, run: yay -Syu infisical" - default: - return "" - } - default: - return "" - } -} - -func getLinuxPackageManager() string { - cmd := exec.Command("apt-get", "--version") - if err := cmd.Run(); err == nil { - return "apt-get" - } - - cmd = exec.Command("yum", "--version") - if err := cmd.Run(); err == nil { - return "yum" - } - - cmd = exec.Command("yay", "--version") - if err := cmd.Run(); err == nil { - return "yay" - } - - cmd = exec.Command("apk", "--version") - if err := cmd.Run(); err == nil { - return "apk" - } - - return "" -} - -func IsRunningInDocker() bool { - if _, err := os.Stat("/.dockerenv"); err == nil { - return true - } - - cgroup, err := ioutil.ReadFile("/proc/self/cgroup") - if err != nil { - return false - } - - return strings.Contains(string(cgroup), "docker") -} - -// func daysSinceDate(dateString string) (int, error) { -// layout := "2006-01-02T15:04:05Z" -// parsedDate, err := time.Parse(layout, dateString) -// if err != nil { -// return 0, err -// } - -// currentTime := time.Now() -// difference := currentTime.Sub(parsedDate) -// days := int(difference.Hours() / 24) -// return days, nil -// } diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go deleted file mode 100644 index 07618ae87..000000000 --- a/cli/packages/util/common.go +++ /dev/null @@ -1,117 +0,0 @@ -package util - -import ( - "fmt" - "net/http" - "os" - "strings" - "unicode" - - "github.com/Infisical/infisical-merge/packages/config" - "github.com/go-resty/resty/v2" -) - -func GetHomeDir() (string, error) { - directory, err := os.UserHomeDir() - return directory, err -} - -// write file to given path. If path does not exist throw error -func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) error { - err := os.WriteFile(fileName, dataToWrite, filePerm) - if err != nil { - return fmt.Errorf("unable to wrote to file [err=%v]", err) - } - - return nil -} - -func ValidateInfisicalAPIConnection() (ok bool) { - _, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL)) - return err == nil -} - -func GetRestyClientWithCustomHeaders() (*resty.Client, error) { - httpClient := resty.New() - customHeaders := os.Getenv("INFISICAL_CUSTOM_HEADERS") - if customHeaders != "" { - headers, err := GetInfisicalCustomHeadersMap() - if err != nil { - return nil, err - } - - httpClient.SetHeaders(headers) - } - return httpClient, nil -} - -func GetInfisicalCustomHeadersMap() (map[string]string, error) { - customHeaders := os.Getenv("INFISICAL_CUSTOM_HEADERS") - if customHeaders == "" { - return nil, nil - } - - headers := map[string]string{} - - pos := 0 - for pos < len(customHeaders) { - for pos < len(customHeaders) && unicode.IsSpace(rune(customHeaders[pos])) { - pos++ - } - - if pos >= len(customHeaders) { - break - } - - keyStart := pos - for pos < len(customHeaders) && customHeaders[pos] != '=' && !unicode.IsSpace(rune(customHeaders[pos])) { - pos++ - } - - if pos >= len(customHeaders) || customHeaders[pos] != '=' { - return nil, fmt.Errorf("invalid custom header format. Expected \"headerKey1=value1 headerKey2=value2 ....\" but got %v", customHeaders) - } - - key := customHeaders[keyStart:pos] - pos++ - - for pos < len(customHeaders) && unicode.IsSpace(rune(customHeaders[pos])) { - pos++ - } - - var value string - - if pos < len(customHeaders) { - if customHeaders[pos] == '"' || customHeaders[pos] == '\'' { - quoteChar := customHeaders[pos] - pos++ - valueStart := pos - - for pos < len(customHeaders) && - (customHeaders[pos] != quoteChar || - (pos > 0 && customHeaders[pos-1] == '\\')) { - pos++ - } - - if pos < len(customHeaders) { - value = customHeaders[valueStart:pos] - pos++ - } else { - value = customHeaders[valueStart:] - } - } else { - valueStart := pos - for pos < len(customHeaders) && !unicode.IsSpace(rune(customHeaders[pos])) { - pos++ - } - value = customHeaders[valueStart:pos] - } - } - - if key != "" && !strings.EqualFold(key, "User-Agent") && !strings.EqualFold(key, "Accept") { - headers[key] = value - } - } - - return headers, nil -} diff --git a/cli/packages/util/config.go b/cli/packages/util/config.go deleted file mode 100644 index 8d44c84d1..000000000 --- a/cli/packages/util/config.go +++ /dev/null @@ -1,259 +0,0 @@ -package util - -import ( - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/rs/zerolog/log" -) - -func WriteInitalConfig(userCredentials *models.UserCredentials) error { - fullConfigFilePath, fullConfigFileDirPath, err := GetFullConfigFilePath() - if err != nil { - return err - } - - // create directory - if _, err := os.Stat(fullConfigFileDirPath); errors.Is(err, os.ErrNotExist) { - err := os.Mkdir(fullConfigFileDirPath, os.ModePerm) - if err != nil { - return err - } - } - - // get existing config - existingConfigFile, err := GetConfigFile() - if err != nil { - return fmt.Errorf("writeInitalConfig: unable to write config file because [err=%s]", err) - } - - //if profiles exists - loggedInUser := models.LoggedInUser{ - Email: userCredentials.Email, - Domain: config.INFISICAL_URL, - } - //if empty or if email not in loggedinUsers - if len(existingConfigFile.LoggedInUsers) == 0 || !ConfigContainsEmail(existingConfigFile.LoggedInUsers, userCredentials.Email) { - existingConfigFile.LoggedInUsers = append(existingConfigFile.LoggedInUsers, loggedInUser) - } else { - //if exists update domain of loggedin users - for idx, user := range existingConfigFile.LoggedInUsers { - if user.Email == userCredentials.Email { - existingConfigFile.LoggedInUsers[idx] = loggedInUser - } - } - } - - configFile := models.ConfigFile{ - LoggedInUserEmail: userCredentials.Email, - LoggedInUserDomain: config.INFISICAL_URL, - LoggedInUsers: existingConfigFile.LoggedInUsers, - VaultBackendType: existingConfigFile.VaultBackendType, - VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase, - Domains: existingConfigFile.Domains, - } - - configFileMarshalled, err := json.Marshal(configFile) - if err != nil { - return err - } - - // Create file in directory - err = WriteToFile(fullConfigFilePath, configFileMarshalled, 0600) - if err != nil { - return err - } - - return err -} - -func ConfigFileExists() bool { - fullConfigFileURI, _, err := GetFullConfigFilePath() - if err != nil { - log.Debug().Err(err).Msgf("There was an error when creating the full path to config file") - return false - } - - if _, err := os.Stat(fullConfigFileURI); err == nil { - return true - } else { - return false - } -} - -func WorkspaceConfigFileExistsInCurrentPath() bool { - if _, err := os.Stat(INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil { - return true - } else { - log.Debug().Err(err) - return false - } -} - -func GetWorkSpaceFromFile() (models.WorkspaceConfigFile, error) { - cfgFile, err := FindWorkspaceConfigFile() - if err != nil { - return models.WorkspaceConfigFile{}, err - } - - configFileAsBytes, err := os.ReadFile(cfgFile) - if err != nil { - return models.WorkspaceConfigFile{}, err - } - - var workspaceConfigFile models.WorkspaceConfigFile - err = json.Unmarshal(configFileAsBytes, &workspaceConfigFile) - if err != nil { - return models.WorkspaceConfigFile{}, err - } - - return workspaceConfigFile, nil -} - -func GetWorkSpaceFromFilePath(configFileDir string) (models.WorkspaceConfigFile, error) { - configFilePath := filepath.Join(configFileDir, ".infisical.json") - - _, configFileStatusError := os.Stat(configFilePath) - if os.IsNotExist(configFileStatusError) { - return models.WorkspaceConfigFile{}, fmt.Errorf("file %s does not exist", configFilePath) - } - - configFileAsBytes, err := os.ReadFile(configFilePath) - if err != nil { - return models.WorkspaceConfigFile{}, err - } - - var workspaceConfigFile models.WorkspaceConfigFile - err = json.Unmarshal(configFileAsBytes, &workspaceConfigFile) - if err != nil { - return models.WorkspaceConfigFile{}, err - } - - return workspaceConfigFile, nil -} - -// FindWorkspaceConfigFile searches for a .infisical.json file in the current directory and all parent directories. -func FindWorkspaceConfigFile() (string, error) { - dir, err := os.Getwd() - if err != nil { - return "", err - } - - for { - path := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - _, err := os.Stat(path) - if err == nil { - // file found - log.Debug().Msgf("FindWorkspaceConfigFile: workspace file found at [path=%s]", path) - - return path, nil - } - - // check if we have reached the root directory - if dir == filepath.Dir(dir) { - break - } - - // move up one directory - dir = filepath.Dir(dir) - } - - // file not found - return "", fmt.Errorf("file not found: %s", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - -} - -func GetFullConfigFilePath() (fullPathToFile string, fullPathToDirectory string, err error) { - homeDir, err := GetHomeDir() - if err != nil { - return "", "", err - } - - fullPath := fmt.Sprintf("%s/%s/%s", homeDir, CONFIG_FOLDER_NAME, CONFIG_FILE_NAME) - fullDirPath := fmt.Sprintf("%s/%s", homeDir, CONFIG_FOLDER_NAME) - return fullPath, fullDirPath, err -} - -// Given a path to a workspace config, unmarshal workspace config -func GetWorkspaceConfigByPath(path string) (workspaceConfig models.WorkspaceConfigFile, err error) { - workspaceConfigFileAsBytes, err := os.ReadFile(path) - if err != nil { - return models.WorkspaceConfigFile{}, fmt.Errorf("GetWorkspaceConfigByPath: Unable to read workspace config file because [%s]", err) - } - - var workspaceConfigFile models.WorkspaceConfigFile - err = json.Unmarshal(workspaceConfigFileAsBytes, &workspaceConfigFile) - if err != nil { - return models.WorkspaceConfigFile{}, fmt.Errorf("GetWorkspaceConfigByPath: Unable to unmarshal workspace config file because [%s]", err) - } - - return workspaceConfigFile, nil -} - -// Get the infisical config file and if it doesn't exist, return empty config model, otherwise raise error -func GetConfigFile() (models.ConfigFile, error) { - fullConfigFilePath, _, err := GetFullConfigFilePath() - if err != nil { - return models.ConfigFile{}, err - } - - configFileAsBytes, err := os.ReadFile(fullConfigFilePath) - if err != nil { - if err, ok := err.(*os.PathError); ok { - return models.ConfigFile{}, nil - } else { - return models.ConfigFile{}, err - } - } - - var configFile models.ConfigFile - err = json.Unmarshal(configFileAsBytes, &configFile) - if err != nil { - return models.ConfigFile{}, err - } - - if configFile.VaultBackendPassphrase != "" { - decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase) - if err != nil { - return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err) - } - os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase)) - } - - return configFile, nil -} - -// Write a ConfigFile to disk. Raise error if unable to save the model to disk -func WriteConfigFile(configFile *models.ConfigFile) error { - fullConfigFilePath, fullConfigFileDirPath, err := GetFullConfigFilePath() - if err != nil { - return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when getting config file path [err=%s]", err) - } - - configFileMarshalled, err := json.Marshal(configFile) - if err != nil { - return fmt.Errorf("writeConfigFile: unable to write config file because an error occurred when marshalling the config file [err=%s]", err) - } - - // check if config folder exists and if not create it - if _, err := os.Stat(fullConfigFileDirPath); errors.Is(err, os.ErrNotExist) { - err := os.Mkdir(fullConfigFileDirPath, os.ModePerm) - if err != nil { - return err - } - } - - // Create file in directory - err = os.WriteFile(fullConfigFilePath, configFileMarshalled, 0600) - if err != nil { - return fmt.Errorf("writeConfigFile: Unable to write to file [err=%s]", err) - } - - return nil -} diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go deleted file mode 100644 index 383c7fc4c..000000000 --- a/cli/packages/util/constants.go +++ /dev/null @@ -1,63 +0,0 @@ -package util - -const ( - CONFIG_FILE_NAME = "infisical-config.json" - CONFIG_FOLDER_NAME = ".infisical" - INFISICAL_DEFAULT_US_URL = "https://app.infisical.com" - INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" - INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" - INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" - INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN" - INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase. - - INFISICAL_BOOTSTRAP_EMAIL_NAME = "INFISICAL_ADMIN_EMAIL" - INFISICAL_BOOTSTRAP_PASSWORD_NAME = "INFISICAL_ADMIN_PASSWORD" - INFISICAL_BOOTSTRAP_ORGANIZATION_NAME = "INFISICAL_ADMIN_ORGANIZATION" - - VAULT_BACKEND_AUTO_MODE = "auto" - VAULT_BACKEND_FILE_MODE = "file" - - INFISICAL_AUTH_METHOD_NAME = "INFISICAL_AUTH_METHOD" - - // Universal Auth - INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID" - INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET" - - // Kubernetes auth - INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME = "INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH" - - // GCP Auth - INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME = "INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH" - - // OIDC Auth - INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT" // deprecated in favor of INFISICAL_JWT - - // JWT AUTH - INFISICAL_JWT_NAME = "INFISICAL_JWT" - - INFISICAL_GATEWAY_TOKEN_NAME_LEGACY = "TOKEN" // backwards compatibility with gateway helm chart, where token was the only supported auth method - - // Generic env variable used for auth methods that require a machine identity ID - INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID" - - SECRET_TYPE_PERSONAL = "personal" - SECRET_TYPE_SHARED = "shared" - KEYRING_SERVICE_NAME = "infisical" - PERSONAL_SECRET_TYPE_NAME = "personal" - SHARED_SECRET_TYPE_NAME = "shared" - - SERVICE_TOKEN_IDENTIFIER = "service-token" - UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token" - - INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30 - INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key" - - KUBERNETES_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST" - KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME = "KUBERNETES_SERVICE_PORT_HTTPS" - KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" -) - -var ( - CLI_VERSION = "devel" -) diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go deleted file mode 100644 index cd73e47ca..000000000 --- a/cli/packages/util/credentials.go +++ /dev/null @@ -1,127 +0,0 @@ -package util - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/zalando/go-keyring" -) - -type LoggedInUserDetails struct { - IsUserLoggedIn bool - LoginExpired bool - UserCredentials models.UserCredentials -} - -func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { - userCredMarshalled, err := json.Marshal(userCred) - if err != nil { - return fmt.Errorf("StoreUserCredsInKeyRing: something went wrong when marshalling user creds [err=%s]", err) - } - - err = SetValueInKeyring(userCred.Email, string(userCredMarshalled)) - if err != nil { - return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err) - } - - return err -} - -func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentials, err error) { - credentialsValue, err := GetValueInKeyring(userEmail) - if err != nil { - if err == keyring.ErrUnsupportedPlatform { - return models.UserCredentials{}, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token") - } else if err == keyring.ErrNotFound { - return models.UserCredentials{}, errors.New("credentials not found in system keyring") - } else { - return models.UserCredentials{}, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err) - } - } - - var userCredentials models.UserCredentials - - err = json.Unmarshal([]byte(credentialsValue), &userCredentials) - if err != nil { - return models.UserCredentials{}, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err) - } - - return userCredentials, err -} - -func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails, error) { - if ConfigFileExists() { - configFile, err := GetConfigFile() - if err != nil { - return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to get logged in user from config file [err=%s]", err) - } - - if configFile.LoggedInUserEmail == "" { - return LoggedInUserDetails{}, nil - } - - userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail) - if err != nil { - if strings.Contains(err.Error(), "credentials not found in system keyring") { - return LoggedInUserDetails{}, errors.New("we couldn't find your logged in details, try running [infisical login] then try again") - } else { - return LoggedInUserDetails{}, fmt.Errorf("failed to fetch credentials from keyring because [err=%s]", err) - } - } - - if setConfigVariables { - config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL - //configFile.LoggedInUserDomain - //if not empty set as infisical url - if configFile.LoggedInUserDomain != "" { - config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain) - } - } - - // check to to see if the JWT is still valid - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to get client with custom headers [err=%s]", err) - } - - httpClient. - SetAuthToken(userCreds.JTWToken). - SetHeader("Accept", "application/json") - - isAuthenticated := api.CallIsAuthenticated(httpClient) - // TODO: add refresh token - // if !isAuthenticated { - // accessTokenResponse, err := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) - // if err == nil && accessTokenResponse.Token != "" { - // isAuthenticated = true - // userCreds.JTWToken = accessTokenResponse.Token - // } - // } - - // err = StoreUserCredsInKeyRing(&userCreds) - // if err != nil { - // log.Debug().Msg("unable to store your user credentials with new access token") - // } - - if !isAuthenticated { - return LoggedInUserDetails{ - IsUserLoggedIn: true, // was logged in - LoginExpired: true, - UserCredentials: userCreds, - }, nil - } - - return LoggedInUserDetails{ - IsUserLoggedIn: true, - LoginExpired: false, - UserCredentials: userCreds, - }, nil - } else { - return LoggedInUserDetails{}, nil - } -} diff --git a/cli/packages/util/exec.go b/cli/packages/util/exec.go deleted file mode 100644 index 2cdb50f42..000000000 --- a/cli/packages/util/exec.go +++ /dev/null @@ -1,92 +0,0 @@ -package util - -import ( - "fmt" - "os" - "os/exec" - "os/signal" - "runtime" - "syscall" -) - -func RunCommand(singleCommand string, args []string, env []string, waitForExit bool) (*exec.Cmd, error) { - var c *exec.Cmd - var err error - - if singleCommand != "" { - c, err = RunCommandFromString(singleCommand, env, waitForExit) - } else { - c, err = RunCommandFromArgs(args, env, waitForExit) - } - - return c, err -} - -func IsProcessRunning(p *os.Process) bool { - err := p.Signal(syscall.Signal(0)) - return err == nil -} - -// For "infisical run -- COMMAND" -func RunCommandFromArgs(args []string, env []string, waitForExit bool) (*exec.Cmd, error) { - cmd := exec.Command(args[0], args[1:]...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Env = env - - err := execCommand(cmd, waitForExit) - - return cmd, err -} - -func execCommand(cmd *exec.Cmd, waitForExit bool) error { - sigChannel := make(chan os.Signal, 1) - signal.Notify(sigChannel) - - if err := cmd.Start(); err != nil { - return err - } - - go func() { - for { - sig := <-sigChannel - _ = cmd.Process.Signal(sig) // process all sigs - } - }() - - if !waitForExit { - return nil - } - - if err := cmd.Wait(); err != nil { - _ = cmd.Process.Signal(os.Kill) - return fmt.Errorf("failed to wait for command termination: %v", err) - } - - waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) - os.Exit(waitStatus.ExitStatus()) - return nil -} - -// For "infisical run --command=COMMAND" -func RunCommandFromString(command string, env []string, waitForExit bool) (*exec.Cmd, error) { - shell := [2]string{"sh", "-c"} - if runtime.GOOS == "windows" { - shell = [2]string{"cmd", "/C"} - } else { - currentShell := os.Getenv("SHELL") - if currentShell != "" { - shell[0] = currentShell - } - } - - cmd := exec.Command(shell[0], shell[1], command) // #nosec G204 nosemgrep: semgrep_configs.prohibit-exec-command - cmd.Env = env - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := execCommand(cmd, waitForExit) - return cmd, err -} diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go deleted file mode 100644 index fb4f2a322..000000000 --- a/cli/packages/util/folders.go +++ /dev/null @@ -1,281 +0,0 @@ -package util - -import ( - "fmt" - "strings" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/rs/zerolog/log" -) - -func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder, error) { - - var foldersToReturn []models.SingleFolder - var folderErr error - if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" { - RequireLogin() - - log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details") - - loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) - if err != nil { - return nil, err - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = EstablishUserLoginSession() - } - - if params.WorkspaceId == "" { - workspaceFile, err := GetWorkSpaceFromFile() - if err != nil { - PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - params.WorkspaceId = workspaceFile.WorkspaceId - } - - folders, err := GetFoldersViaJTW(loggedInUserDetails.UserCredentials.JTWToken, params.WorkspaceId, params.Environment, params.FoldersPath) - folderErr = err - foldersToReturn = folders - } else if params.InfisicalToken != "" { - log.Debug().Msg("GetAllFolders: Trying to fetch folders using service token") - - // get folders via service token - folders, err := GetFoldersViaServiceToken(params.InfisicalToken, params.WorkspaceId, params.Environment, params.FoldersPath) - folderErr = err - foldersToReturn = folders - } else if params.UniversalAuthAccessToken != "" { - log.Debug().Msg("GetAllFolders: Trying to fetch folders using universal auth") - - if params.WorkspaceId == "" { - PrintErrorMessageAndExit("Project ID is required when using machine identity") - } - - // get folders via machine identity - folders, err := GetFoldersViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.FoldersPath) - folderErr = err - foldersToReturn = folders - } - return foldersToReturn, folderErr -} - -func GetFoldersViaJTW(JTWToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) { - // set up resty client - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return nil, err - } - - httpClient.SetAuthToken(JTWToken). - SetHeader("Accept", "application/json") - - getFoldersRequest := api.GetFoldersV1Request{ - WorkspaceId: workspaceId, - Environment: environmentName, - FoldersPath: foldersPath, - } - - apiResponse, err := api.CallGetFoldersV1(httpClient, getFoldersRequest) - if err != nil { - return nil, err - } - - var folders []models.SingleFolder - - for _, folder := range apiResponse.Folders { - folders = append(folders, models.SingleFolder{ - Name: folder.Name, - ID: folder.ID, - }) - } - - return folders, nil -} - -func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) { - serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) - if len(serviceTokenParts) < 4 { - return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") - } - - serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) - - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err) - } - - httpClient.SetAuthToken(serviceToken). - SetHeader("Accept", "application/json") - - serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient) - if err != nil { - return nil, fmt.Errorf("unable to get service token details. [err=%v]", err) - } - - // if multiple scopes are there then user needs to specify which environment and folder path - if environmentName == "" { - if len(serviceTokenDetails.Scopes) != 1 { - return nil, fmt.Errorf("you need to provide the --env for multiple environment scoped token") - } else { - environmentName = serviceTokenDetails.Scopes[0].Environment - } - } - - getFoldersRequest := api.GetFoldersV1Request{ - WorkspaceId: serviceTokenDetails.Workspace, - Environment: environmentName, - FoldersPath: foldersPath, - } - - apiResponse, err := api.CallGetFoldersV1(httpClient, getFoldersRequest) - if err != nil { - return nil, fmt.Errorf("unable to get folders. [err=%v]", err) - } - - var folders []models.SingleFolder - - for _, folder := range apiResponse.Folders { - folders = append(folders, models.SingleFolder{ - Name: folder.Name, - ID: folder.ID, - }) - } - - return folders, nil -} - -func GetFoldersViaMachineIdentity(accessToken string, workspaceId string, envSlug string, foldersPath string) ([]models.SingleFolder, error) { - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return nil, err - } - - httpClient.SetAuthToken(accessToken). - SetHeader("Accept", "application/json") - - getFoldersRequest := api.GetFoldersV1Request{ - WorkspaceId: workspaceId, - Environment: envSlug, - FoldersPath: foldersPath, - } - - apiResponse, err := api.CallGetFoldersV1(httpClient, getFoldersRequest) - if err != nil { - return nil, err - } - - var folders []models.SingleFolder - - for _, folder := range apiResponse.Folders { - folders = append(folders, models.SingleFolder{ - Name: folder.Name, - ID: folder.ID, - }) - } - - return folders, nil -} - -// CreateFolder creates a folder in Infisical -func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, error) { - - // If no token is provided, we will try to get the token from the current logged in user - if params.InfisicalToken == "" { - RequireLogin() - loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) - - if err != nil { - return models.SingleFolder{}, err - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = EstablishUserLoginSession() - } - - params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - // set up resty client - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return models.SingleFolder{}, err - } - - httpClient.SetAuthToken(params.InfisicalToken). - SetHeader("Accept", "application/json"). - SetHeader("Content-Type", "application/json") - - createFolderRequest := api.CreateFolderV1Request{ - WorkspaceId: params.WorkspaceId, - Environment: params.Environment, - FolderName: params.FolderName, - Path: params.FolderPath, - } - - apiResponse, err := api.CallCreateFolderV1(httpClient, createFolderRequest) - if err != nil { - return models.SingleFolder{}, err - } - - folder := apiResponse.Folder - - return models.SingleFolder{ - Name: folder.Name, - ID: folder.ID, - }, nil -} - -func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, error) { - - // If no token is provided, we will try to get the token from the current logged in user - if params.InfisicalToken == "" { - RequireLogin() - - loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) - - if err != nil { - return nil, err - } - - if loggedInUserDetails.LoginExpired { - loggedInUserDetails = EstablishUserLoginSession() - } - - params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken - } - - // set up resty client - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return nil, err - } - - httpClient.SetAuthToken(params.InfisicalToken). - SetHeader("Accept", "application/json"). - SetHeader("Content-Type", "application/json") - - deleteFolderRequest := api.DeleteFolderV1Request{ - WorkspaceId: params.WorkspaceId, - Environment: params.Environment, - FolderName: params.FolderName, - Directory: params.FolderPath, - } - - apiResponse, err := api.CallDeleteFolderV1(httpClient, deleteFolderRequest) - if err != nil { - return nil, err - } - - var folders []models.SingleFolder - - for _, folder := range apiResponse.Folders { - folders = append(folders, models.SingleFolder{ - Name: folder.Name, - ID: folder.ID, - }) - } - - return folders, nil -} diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go deleted file mode 100644 index abd9768aa..000000000 --- a/cli/packages/util/helper.go +++ /dev/null @@ -1,338 +0,0 @@ -package util - -import ( - "bytes" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "fmt" - "math/rand" - "os" - "os/exec" - "path" - "sort" - "strings" - "time" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/spf13/cobra" -) - -type DecodedSymmetricEncryptionDetails = struct { - Cipher []byte - IV []byte - Tag []byte - Key []byte -} - -const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - -func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) { - cipherx, err := base64.StdEncoding.DecodeString(cipher) - if err != nil { - return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode cipher text [err=%v]", err) - } - - keyx, err := base64.StdEncoding.DecodeString(key) - if err != nil { - return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode key [err=%v]", err) - } - - IVx, err := base64.StdEncoding.DecodeString(IV) - if err != nil { - return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode IV [err=%v]", err) - } - - tagx, err := base64.StdEncoding.DecodeString(tag) - if err != nil { - return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode tag [err=%v]", err) - } - - return DecodedSymmetricEncryptionDetails{ - Key: keyx, - Cipher: cipherx, - IV: IVx, - Tag: tagx, - }, nil -} - -// Helper function to sort the secrets by key so we can create a consistent output -func SortSecretsByKeys(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable { - sort.Slice(secrets, func(i, j int) bool { - return secrets[i].Key < secrets[j].Key - }) - return secrets -} - -func IsSecretEnvironmentValid(env string) bool { - if env == "prod" || env == "dev" || env == "test" || env == "staging" { - return true - } - return false -} - -func IsSecretTypeValid(s string) bool { - if s == "personal" || s == "shared" { - return true - } - return false -} - -func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err error) { - infisicalToken, err := cmd.Flags().GetString("token") - - if err != nil { - return nil, err - } - - var source = "--token flag" - - if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable. - infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME) - source = fmt.Sprintf("%s environment variable", INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME) - - if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable. - infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) - source = fmt.Sprintf("%s environment variable", INFISICAL_TOKEN_NAME) - } - - if infisicalToken == "" { // if its still empty, check for the `TOKEN` environment variable (for gateway helm) - infisicalToken = os.Getenv(INFISICAL_GATEWAY_TOKEN_NAME_LEGACY) - source = fmt.Sprintf("%s environment variable", INFISICAL_GATEWAY_TOKEN_NAME_LEGACY) - } - } - - if infisicalToken == "" { // If it's empty, we return nothing at all. - return nil, nil - } - - if strings.HasPrefix(infisicalToken, "st.") { - return &models.TokenDetails{ - Type: SERVICE_TOKEN_IDENTIFIER, - Token: infisicalToken, - Source: source, - }, nil - } - - return &models.TokenDetails{ - Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER, - Token: infisicalToken, - Source: source, - }, nil - -} - -func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuthLoginResponse, error) { - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return api.UniversalAuthLoginResponse{}, err - } - - httpClient.SetRetryCount(10000). - SetRetryMaxWaitTime(20 * time.Second). - SetRetryWaitTime(5 * time.Second) - - tokenResponse, err := api.CallUniversalAuthLogin(httpClient, api.UniversalAuthLoginRequest{ClientId: clientId, ClientSecret: clientSecret}) - if err != nil { - return api.UniversalAuthLoginResponse{}, err - } - - return tokenResponse, nil -} - -func RenewMachineIdentityAccessToken(accessToken string) (string, error) { - - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return "", err - } - - httpClient.SetRetryCount(10000). - SetRetryMaxWaitTime(20 * time.Second). - SetRetryWaitTime(5 * time.Second) - - request := api.UniversalAuthRefreshRequest{ - AccessToken: accessToken, - } - - tokenResponse, err := api.CallMachineIdentityRefreshAccessToken(httpClient, request) - if err != nil { - return "", err - } - - return tokenResponse.AccessToken, nil -} - -// Checks if the passed in email already exists in the users slice -func ConfigContainsEmail(users []models.LoggedInUser, email string) bool { - for _, value := range users { - if value.Email == email { - return true - } - } - return false -} - -func RequireLogin() { - // get the config file that stores the current logged in user email - configFile, _ := GetConfigFile() - - if configFile.LoggedInUserEmail == "" { - EstablishUserLoginSession() - } -} - -func IsLoggedIn() bool { - configFile, _ := GetConfigFile() - return configFile.LoggedInUserEmail != "" -} - -func RequireServiceToken() { - serviceToken := os.Getenv(INFISICAL_TOKEN_NAME) - if serviceToken == "" { - PrintErrorMessageAndExit("No service token is found in your terminal") - } -} - -func RequireLocalWorkspaceFile() { - workspaceFilePath, _ := FindWorkspaceConfigFile() - if workspaceFilePath == "" { - PrintErrorMessageAndExit("It looks you have not yet connected this project to Infisical", "To do so, run [infisical init] then run your command again") - } - - workspaceFile, err := GetWorkSpaceFromFile() - if err != nil { - HandleError(err, "Unable to read your project configuration, please try initializing this project again.", "Run [infisical init]") - } - - if workspaceFile.WorkspaceId == "" { - PrintErrorMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]") - } -} - -func ValidateWorkspaceFile(projectConfigFilePath string) { - workspaceFilePath, err := GetWorkSpaceFromFilePath(projectConfigFilePath) - if err != nil { - PrintErrorMessageAndExit(fmt.Sprintf("error reading your project config %v", err)) - } - - if workspaceFilePath.WorkspaceId == "" { - PrintErrorMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]") - } -} - -func GetHashFromStringList(list []string) string { - hash := sha256.New() - - for _, item := range list { - hash.Write([]byte(item)) - } - - sum := sha256.Sum256(hash.Sum(nil)) - return fmt.Sprintf("%x", sum) -} - -// execCmd is a struct that holds the command and arguments to be executed. -// By using this struct, we can easily mock the command and arguments. -type execCmd struct { - cmd string - args []string -} - -var getCurrentBranchCmd = execCmd{ - cmd: "git", - args: []string{"symbolic-ref", "--short", "HEAD"}, -} - -func getCurrentBranch() (string, error) { - cmd := exec.Command(getCurrentBranchCmd.cmd, getCurrentBranchCmd.args...) - var out bytes.Buffer - cmd.Stdout = &out - err := cmd.Run() - if err != nil { - return "", err - } - return path.Base(strings.TrimSpace(out.String())), nil -} - -func AppendAPIEndpoint(address string) string { - // if it's empty return as it is - // Ensure the address does not already end with "/api" - if address == "" || strings.HasSuffix(address, "/api") { - return address - } - - // Check if the address ends with a slash and append accordingly - if address[len(address)-1] == '/' { - return address + "api" - } - return address + "/api" -} - -func ReadFileAsString(filePath string) (string, error) { - fileBytes, err := os.ReadFile(filePath) - - if err != nil { - return "", err - } - - return string(fileBytes), nil - -} - -func GetEnvVarOrFileContent(envName string, filePath string) (string, error) { - // First check if the environment variable is set - if envVarValue := os.Getenv(envName); envVarValue != "" { - return envVarValue, nil - } - - // If it's not set, try to read the file - fileContent, err := ReadFileAsString(filePath) - - if err != nil { - return "", fmt.Errorf("unable to read file content from file path '%s' [err=%v]", filePath, err) - } - - return fileContent, nil -} - -func GetCmdFlagOrEnv(cmd *cobra.Command, flag string, envNames []string) (string, error) { - value, flagsErr := cmd.Flags().GetString(flag) - if flagsErr != nil { - return "", flagsErr - } - if value == "" { - for _, env := range envNames { - value = strings.TrimSpace(os.Getenv(env)) - if value != "" { - break - } - } - } - if value == "" { - return "", fmt.Errorf("please provide %s flag", flag) - } - return value, nil -} - -func GenerateRandomString(length int) string { - b := make([]byte, length) - for i := range b { - b[i] = charset[rand.Intn(len(charset))] - } - return string(b) -} - -func GenerateETagFromSecrets(secrets []models.SingleEnvironmentVariable) string { - sortedSecrets := SortSecretsByKeys(secrets) - content := []byte{} - - for _, secret := range sortedSecrets { - content = append(content, []byte(secret.Key)...) - content = append(content, []byte(secret.Value)...) - } - - hash := sha256.Sum256(content) - return fmt.Sprintf(`"%s"`, hex.EncodeToString(hash[:])) -} diff --git a/cli/packages/util/init.go b/cli/packages/util/init.go deleted file mode 100644 index 4aecb2ab3..000000000 --- a/cli/packages/util/init.go +++ /dev/null @@ -1,46 +0,0 @@ -package util - -import ( - "fmt" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/config" - "github.com/Infisical/infisical-merge/packages/models" -) - -func GetOrganizationsNameList(organizationResponse api.GetOrganizationsResponse) []string { - organizations := organizationResponse.Organizations - - if len(organizations) == 0 { - message := fmt.Sprintf("You don't have any organization created in Infisical. You must first create a organization at %s", config.INFISICAL_URL) - PrintErrorMessageAndExit(message) - } - - var organizationNames []string - for _, workspace := range organizations { - organizationNames = append(organizationNames, workspace.Name) - } - - return organizationNames -} - -func GetWorkspacesInOrganization(workspaceResponse api.GetWorkSpacesResponse, orgId string) ([]models.Workspace, []string) { - workspaces := workspaceResponse.Workspaces - - var filteredWorkspaces []models.Workspace - var workspaceNames []string - - for _, workspace := range workspaces { - if workspace.OrganizationId == orgId { - filteredWorkspaces = append(filteredWorkspaces, workspace) - workspaceNames = append(workspaceNames, workspace.Name) - } - } - - if len(filteredWorkspaces) == 0 { - message := fmt.Sprintf("You don't have any projects created in Infisical organization. You must first create a project at %s", config.INFISICAL_URL) - PrintErrorMessageAndExit(message) - } - - return filteredWorkspaces, workspaceNames -} diff --git a/cli/packages/util/keyringwrapper.go b/cli/packages/util/keyringwrapper.go deleted file mode 100644 index 9c8211a3c..000000000 --- a/cli/packages/util/keyringwrapper.go +++ /dev/null @@ -1,69 +0,0 @@ -package util - -import ( - "encoding/base64" - "fmt" - - "github.com/rs/zerolog/log" - "github.com/zalando/go-keyring" -) - -const MAIN_KEYRING_SERVICE = "infisical-cli" - -type TimeoutError struct { - message string -} - -func (e *TimeoutError) Error() string { - return e.message -} - -func SetValueInKeyring(key, value string) error { - currentVaultBackend, err := GetCurrentVaultBackend() - if err != nil { - PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again") - } - - err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) - - if err != nil { - log.Debug().Msg(fmt.Sprintf("Error while setting default keyring: %v", err)) - configFile, _ := GetConfigFile() - - if configFile.VaultBackendPassphrase == "" { - encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(GenerateRandomString(10))) // generate random passphrase - configFile.VaultBackendPassphrase = encodedPassphrase - configFile.VaultBackendType = VAULT_BACKEND_FILE_MODE - err = WriteConfigFile(&configFile) - if err != nil { - return err - } - - // We call this function at last to trigger the environment variable to be set - GetConfigFile() - } - - err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value) - log.Debug().Msg(fmt.Sprintf("Error while setting file keyring: %v", err)) - } - - return err -} - -func GetValueInKeyring(key string) (string, error) { - currentVaultBackend, err := GetCurrentVaultBackend() - if err != nil { - PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again") - } - return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) - -} - -func DeleteValueInKeyring(key string) error { - currentVaultBackend, err := GetCurrentVaultBackend() - if err != nil { - return err - } - - return keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) -} diff --git a/cli/packages/util/log.go b/cli/packages/util/log.go deleted file mode 100644 index 9e6e558ea..000000000 --- a/cli/packages/util/log.go +++ /dev/null @@ -1,49 +0,0 @@ -package util - -import ( - "fmt" - "os" - - "github.com/fatih/color" -) - -func HandleError(err error, messages ...string) { - PrintErrorAndExit(1, err, messages...) -} - -func PrintErrorAndExit(exitCode int, err error, messages ...string) { - printError(err) - - if len(messages) > 0 { - for _, message := range messages { - fmt.Println(message) - } - } - - supportMsg := fmt.Sprintf("\n\nIf this issue continues, get support at https://infisical.com/slack") - fmt.Fprintln(os.Stderr, supportMsg) - - os.Exit(exitCode) -} - -func PrintWarning(message string) { - color.New(color.FgYellow).Fprintf(os.Stderr, "Warning: %v \n", message) -} - -func PrintSuccessMessage(message string) { - color.New(color.FgGreen).Println(message) -} - -func PrintErrorMessageAndExit(messages ...string) { - if len(messages) > 0 { - for _, message := range messages { - fmt.Fprintln(os.Stderr, message) - } - } - - os.Exit(1) -} - -func printError(e error) { - color.New(color.FgRed).Fprintf(os.Stderr, "error: %v\n", e) -} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go deleted file mode 100644 index 814e7da23..000000000 --- a/cli/packages/util/secrets.go +++ /dev/null @@ -1,824 +0,0 @@ -package util - -import ( - "crypto/rand" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "os" - "strings" - "unicode" - - "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/crypto" - "github.com/Infisical/infisical-merge/packages/models" - "github.com/rs/zerolog/log" - "github.com/zalando/go-keyring" - "gopkg.in/yaml.v3" -) - -func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) ([]models.SingleEnvironmentVariable, error) { - serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) - if len(serviceTokenParts) < 4 { - return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") - } - - serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) - - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err) - } - - httpClient.SetAuthToken(serviceToken). - SetHeader("Accept", "application/json") - - serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient) - if err != nil { - return nil, fmt.Errorf("unable to get service token details. [err=%v]", err) - } - - // if multiple scopes are there then user needs to specify which environment and secret path - if environment == "" { - if len(serviceTokenDetails.Scopes) != 1 { - return nil, fmt.Errorf("you need to provide the --env for multiple environment scoped token") - } else { - environment = serviceTokenDetails.Scopes[0].Environment - } - } - - rawSecrets, err := api.CallGetRawSecretsV3(httpClient, api.GetRawSecretsV3Request{ - WorkspaceId: serviceTokenDetails.Workspace, - Environment: environment, - SecretPath: secretPath, - IncludeImport: includeImports, - Recursive: recursive, - TagSlugs: tagSlugs, - ExpandSecretReferences: expandSecretReferences, - }) - - if err != nil { - return nil, err - } - - plainTextSecrets := []models.SingleEnvironmentVariable{} - - for _, secret := range rawSecrets.Secrets { - plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace}) - } - - if includeImports { - plainTextSecrets, err = InjectRawImportedSecret(plainTextSecrets, rawSecrets.Imports) - if err != nil { - return nil, err - } - } - - return plainTextSecrets, nil - -} - -func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) (models.PlaintextSecretResult, error) { - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return models.PlaintextSecretResult{}, err - } - - httpClient.SetAuthToken(accessToken). - SetHeader("Accept", "application/json") - - getSecretsRequest := api.GetRawSecretsV3Request{ - WorkspaceId: workspaceId, - Environment: environmentName, - IncludeImport: includeImports, - Recursive: recursive, - TagSlugs: tagSlugs, - ExpandSecretReferences: expandSecretReferences, - } - - if secretsPath != "" { - getSecretsRequest.SecretPath = secretsPath - } - - rawSecrets, err := api.CallGetRawSecretsV3(httpClient, getSecretsRequest) - - if err != nil { - return models.PlaintextSecretResult{}, err - } - - plainTextSecrets := []models.SingleEnvironmentVariable{} - - for _, secret := range rawSecrets.Secrets { - plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace, SecretPath: secret.SecretPath}) - } - - if includeImports { - plainTextSecrets, err = InjectRawImportedSecret(plainTextSecrets, rawSecrets.Imports) - if err != nil { - return models.PlaintextSecretResult{}, err - } - } - - return models.PlaintextSecretResult{ - Secrets: plainTextSecrets, - Etag: rawSecrets.ETag, - }, nil -} - -func GetSinglePlainTextSecretByNameV3(accessToken string, workspaceId string, environmentName string, secretsPath string, secretName string) (models.SingleEnvironmentVariable, string, error) { - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return models.SingleEnvironmentVariable{}, "", err - } - - httpClient.SetAuthToken(accessToken). - SetHeader("Accept", "application/json") - - getSecretsRequest := api.GetRawSecretV3ByNameRequest{ - WorkspaceID: workspaceId, - Environment: environmentName, - SecretName: secretName, - SecretPath: secretsPath, - } - - rawSecret, err := api.CallFetchSingleSecretByName(httpClient, getSecretsRequest) - - if err != nil { - return models.SingleEnvironmentVariable{}, "", err - } - - formattedSecrets := models.SingleEnvironmentVariable{ - Key: rawSecret.Secret.SecretKey, - WorkspaceId: rawSecret.Secret.Workspace, - Value: rawSecret.Secret.SecretValue, - Type: rawSecret.Secret.Type, - ID: rawSecret.Secret.ID, - Comment: rawSecret.Secret.SecretComment, - SecretPath: rawSecret.Secret.SecretPath, - } - - return formattedSecrets, rawSecret.ETag, nil -} - -func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string, ttl string) (models.DynamicSecretLease, error) { - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return models.DynamicSecretLease{}, err - } - - httpClient.SetAuthToken(accessToken). - SetHeader("Accept", "application/json") - - dynamicSecretRequest := api.CreateDynamicSecretLeaseV1Request{ - ProjectSlug: projectSlug, - Environment: environmentName, - SecretPath: secretsPath, - Slug: slug, - TTL: ttl, - } - - dynamicSecret, err := api.CallCreateDynamicSecretLeaseV1(httpClient, dynamicSecretRequest) - if err != nil { - return models.DynamicSecretLease{}, err - } - - return models.DynamicSecretLease{ - Lease: dynamicSecret.Lease, - Data: dynamicSecret.Data, - DynamicSecret: dynamicSecret.DynamicSecret, - }, nil -} - -func InjectRawImportedSecret(secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedRawSecretV3) ([]models.SingleEnvironmentVariable, error) { - if importedSecrets == nil { - return secrets, nil - } - - hasOverriden := make(map[string]bool) - for _, sec := range secrets { - hasOverriden[sec.Key] = true - } - - for i := len(importedSecrets) - 1; i >= 0; i-- { - importSec := importedSecrets[i] - plainTextImportedSecrets := importSec.Secrets - - for _, sec := range plainTextImportedSecrets { - if _, ok := hasOverriden[sec.SecretKey]; !ok { - secrets = append(secrets, models.SingleEnvironmentVariable{ - Key: sec.SecretKey, - WorkspaceId: sec.Workspace, - Value: sec.SecretValue, - Type: sec.Type, - ID: sec.ID, - }) - hasOverriden[sec.SecretKey] = true - } - } - } - return secrets, nil -} - -func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tagSlugs string) []models.SingleEnvironmentVariable { - if tagSlugs == "" { - return plainTextSecrets - } - - tagSlugsMap := make(map[string]bool) - tagSlugsList := strings.Split(tagSlugs, ",") - for _, slug := range tagSlugsList { - tagSlugsMap[slug] = true - } - - filteredSecrets := []models.SingleEnvironmentVariable{} - for _, secret := range plainTextSecrets { - for _, tag := range secret.Tags { - if tagSlugsMap[tag.Slug] { - filteredSecrets = append(filteredSecrets, secret) - break - } - } - } - - return filteredSecrets -} - -func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) { - var secretsToReturn []models.SingleEnvironmentVariable - // var serviceTokenDetails api.GetServiceTokenDetailsResponse - var errorToReturn error - - if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" { - if params.WorkspaceId == "" { - if projectConfigFilePath == "" { - _, err := GetWorkSpaceFromFile() - if err != nil { - PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - } else { - ValidateWorkspaceFile(projectConfigFilePath) - } - } - - RequireLogin() - - log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details") - - loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true) - isConnected := ValidateInfisicalAPIConnection() - - if isConnected { - log.Debug().Msg("GetAllEnvironmentVariables: Connected to Infisical instance, checking logged in creds") - } - - if err != nil { - return nil, err - } - - if isConnected && loggedInUserDetails.LoginExpired { - loggedInUserDetails = EstablishUserLoginSession() - } - - if params.WorkspaceId == "" { - var infisicalDotJson models.WorkspaceConfigFile - - if projectConfigFilePath == "" { - projectConfig, err := GetWorkSpaceFromFile() - if err != nil { - PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag") - } - - infisicalDotJson = projectConfig - } else { - projectConfig, err := GetWorkSpaceFromFilePath(projectConfigFilePath) - if err != nil { - return nil, err - } - - infisicalDotJson = projectConfig - } - params.WorkspaceId = infisicalDotJson.WorkspaceId - } - - res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, params.WorkspaceId, - params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs, true) - log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err) - - if err == nil { - backupEncryptionKey, err := GetBackupEncryptionKey() - if err != nil { - return nil, err - } - WriteBackupSecrets(params.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey, res.Secrets) - } - - secretsToReturn = res.Secrets - errorToReturn = err - // only attempt to serve cached secrets if no internet connection and if at least one secret cached - if !isConnected { - backupEncryptionKey, _ := GetBackupEncryptionKey() - if backupEncryptionKey != nil { - backedUpSecrets, err := ReadBackupSecrets(params.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey) - if len(backedUpSecrets) > 0 { - PrintWarning("Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug") - secretsToReturn = backedUpSecrets - errorToReturn = err - } - } - } - - } else { - if params.InfisicalToken != "" { - log.Debug().Msg("Trying to fetch secrets using service token") - secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs, params.ExpandSecretReferences) - } else if params.UniversalAuthAccessToken != "" { - - if params.WorkspaceId == "" { - PrintErrorMessageAndExit("Project ID is required when using machine identity") - } - - log.Debug().Msg("Trying to fetch secrets using universal auth") - res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs, params.ExpandSecretReferences) - - errorToReturn = err - secretsToReturn = res.Secrets - } - } - - return secretsToReturn, errorToReturn -} - -func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { - secretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - - for _, secret := range secrets { - secretMapByName[secret.Key] = secret - } - - return secretMapByName -} - -func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable { - personalSecrets := make(map[string]models.SingleEnvironmentVariable) - sharedSecrets := make(map[string]models.SingleEnvironmentVariable) - secretsToReturn := []models.SingleEnvironmentVariable{} - secretsToReturnMap := make(map[string]models.SingleEnvironmentVariable) - - for _, secret := range secrets { - if secret.Type == PERSONAL_SECRET_TYPE_NAME { - personalSecrets[secret.Key] = secret - } - if secret.Type == SHARED_SECRET_TYPE_NAME { - sharedSecrets[secret.Key] = secret - } - } - - if secretType == PERSONAL_SECRET_TYPE_NAME { - for _, secret := range secrets { - if personalSecret, exists := personalSecrets[secret.Key]; exists { - secretsToReturnMap[secret.Key] = personalSecret - } else { - if _, exists = secretsToReturnMap[secret.Key]; !exists { - secretsToReturnMap[secret.Key] = secret - } - } - } - } else if secretType == SHARED_SECRET_TYPE_NAME { - for _, secret := range secrets { - if sharedSecret, exists := sharedSecrets[secret.Key]; exists { - secretsToReturnMap[secret.Key] = sharedSecret - } else { - if _, exists := secretsToReturnMap[secret.Key]; !exists { - secretsToReturnMap[secret.Key] = secret - } - } - } - } - - for _, secret := range secretsToReturnMap { - secretsToReturn = append(secretsToReturn, secret) - } - return secretsToReturn -} - -func GetBackupEncryptionKey() ([]byte, error) { - encryptionKey, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY) - if err != nil { - if err == keyring.ErrUnsupportedPlatform { - return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token") - } else if err == keyring.ErrNotFound { - // generate a new key - randomizedKey := make([]byte, 16) - rand.Read(randomizedKey) - encryptionKey = hex.EncodeToString(randomizedKey) - if err := SetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY, encryptionKey); err != nil { - return nil, err - } - return []byte(encryptionKey), nil - } else { - return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err) - } - } - return []byte(encryptionKey), nil -} - -func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error { - formattedPath := strings.ReplaceAll(secretsPath, "/", "-") - fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath) - secrets_backup_folder_name := "secrets-backup" - - _, fullConfigFileDirPath, err := GetFullConfigFilePath() - if err != nil { - return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err) - } - - // create secrets backup directory - fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) - if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) { - err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm) - if err != nil { - return err - } - } - marshaledSecrets, _ := json.Marshal(secrets) - result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey) - if err != nil { - return fmt.Errorf("WriteBackupSecrets: Unable to encrypt local secret backup to file [err=%s]", err) - } - listOfSecretsMarshalled, _ := json.Marshal(result) - err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600) - if err != nil { - return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err) - } - - return nil -} - -func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) { - formattedPath := strings.ReplaceAll(secretsPath, "/", "-") - fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath) - secrets_backup_folder_name := "secrets-backup" - - _, fullConfigFileDirPath, err := GetFullConfigFilePath() - if err != nil { - return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err) - } - - fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) - if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) { - return nil, nil - } - - encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName) - - encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath) - if err != nil { - return nil, err - } - - var encryptedBackUpSecrets models.SymmetricEncryptionResult - err = json.Unmarshal(encryptedBackupSecretsAsBytes, &encryptedBackUpSecrets) - if err != nil { - return nil, fmt.Errorf("ReadBackupSecrets: unable to parse encrypted backup secrets. The secrets backup may be malformed [err=%s]", err) - } - - result, err := crypto.DecryptSymmetric(encryptionKey, encryptedBackUpSecrets.CipherText, encryptedBackUpSecrets.AuthTag, encryptedBackUpSecrets.Nonce) - if err != nil { - return nil, fmt.Errorf("ReadBackupSecrets: unable to decrypt encrypted backup secrets [err=%s]", err) - } - var plainTextSecrets []models.SingleEnvironmentVariable - _ = json.Unmarshal(result, &plainTextSecrets) - - return plainTextSecrets, nil - -} - -func DeleteBackupSecrets() error { - secrets_backup_folder_name := "secrets-backup" - - _, fullConfigFileDirPath, err := GetFullConfigFilePath() - if err != nil { - return fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err) - } - - fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) - DeleteValueInKeyring(INFISICAL_BACKUP_SECRET) - DeleteValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY) - - return os.RemoveAll(fullPathToSecretsBackupFolder) -} - -func GetEnvFromWorkspaceFile() string { - workspaceFile, err := GetWorkSpaceFromFile() - if err != nil { - log.Debug().Msgf("getEnvFromWorkspaceFile: [err=%s]", err) - return "" - } - - if env := GetEnvelopmentBasedOnGitBranch(workspaceFile); env != "" { - return env - } - - return workspaceFile.DefaultEnvironment -} - -func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) string { - branch, err := getCurrentBranch() - if err != nil { - log.Debug().Msgf("getEnvelopmentBasedOnGitBranch: [err=%s]", err) - } - - envBasedOnGitBranch, ok := workspaceFile.GitBranchToEnvironmentMapping[branch] - - log.Debug().Msgf("GetEnvelopmentBasedOnGitBranch: [envBasedOnGitBranch=%s] [ok=%t]", envBasedOnGitBranch, ok) - - if err == nil && ok { - return envBasedOnGitBranch - } else { - log.Debug().Msgf("getEnvelopmentBasedOnGitBranch: [err=%s]", err) - return "" - } -} - -func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey string, workspaceId string) ([]byte, error) { - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return nil, fmt.Errorf("GetPlainTextWorkspaceKey: unable to get client with custom headers [err=%v]", err) - } - - httpClient.SetAuthToken(authenticationToken). - SetHeader("Accept", "application/json") - - request := api.GetEncryptedWorkspaceKeyRequest{ - WorkspaceId: workspaceId, - } - - workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request) - if err != nil { - return nil, fmt.Errorf("GetPlainTextWorkspaceKey: unable to retrieve your encrypted workspace key. [err=%v]", err) - } - - encryptedWorkspaceKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey) - if err != nil { - return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKey [err=%v]", err) - } - - encryptedWorkspaceKeySenderPublicKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey) - if err != nil { - return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKeySenderPublicKey [err=%v]", err) - } - - encryptedWorkspaceKeyNonce, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce) - if err != nil { - return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKeyNonce [err=%v]", err) - } - - currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(receiverPrivateKey) - if err != nil { - return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for currentUsersPrivateKey [err=%v]", err) - } - - if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 { - return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Missing credentials for generating plainTextEncryptionKey") - } - - return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil -} - -func parseSecrets(fileName string, content string) (map[string]string, error) { - secrets := make(map[string]string) - - if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") { - // Handle YAML secrets - var yamlData map[string]interface{} - if err := yaml.Unmarshal([]byte(content), &yamlData); err != nil { - return nil, fmt.Errorf("failed to parse YAML file: %v", err) - } - - for key, value := range yamlData { - if strValue, ok := value.(string); ok { - secrets[key] = strValue - } else { - return nil, fmt.Errorf("YAML secret '%s' must be a string", key) - } - } - } else { - // Handle .env files - lines := strings.Split(content, "\n") - - for _, line := range lines { - line = strings.TrimSpace(line) - - // Ignore empty lines and comments - if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { - continue - } - - // Ensure it's a valid key=value pair - splitKeyValue := strings.SplitN(line, "=", 2) - if len(splitKeyValue) != 2 { - return nil, fmt.Errorf("invalid format, expected key=value in line: %s", line) - } - - key, value := strings.TrimSpace(splitKeyValue[0]), strings.TrimSpace(splitKeyValue[1]) - - // Handle quoted values - if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) || - (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) { - value = value[1 : len(value)-1] // Remove surrounding quotes - } - - secrets[key] = value - } - } - - return secrets, nil -} - -func validateSecretKey(key string) error { - if key == "" { - return errors.New("secret keys cannot be empty") - } - if unicode.IsNumber(rune(key[0])) { - return fmt.Errorf("secret key '%s' cannot start with a number", key) - } - if strings.Contains(key, " ") { - return fmt.Errorf("secret key '%s' cannot contain spaces", key) - } - return nil -} - -func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) { - if file != "" { - content, err := os.ReadFile(file) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - PrintErrorMessageAndExit("File does not exist") - } - return nil, fmt.Errorf("unable to process file [err=%v]", err) - } - - parsedSecrets, err := parseSecrets(file, string(content)) - if err != nil { - PrintErrorMessageAndExit(fmt.Sprintf("error parsing secrets: %v", err)) - } - - // Step 2: Validate secrets - for key, value := range parsedSecrets { - if err := validateSecretKey(key); err != nil { - PrintErrorMessageAndExit(err.Error()) - } - if strings.TrimSpace(value) == "" { - PrintErrorMessageAndExit(fmt.Sprintf("Secret key '%s' has an empty value", key)) - } - secretArgs = append(secretArgs, fmt.Sprintf("%s=%s", key, value)) - } - - if len(secretArgs) == 0 { - PrintErrorMessageAndExit("no valid secrets found in the file") - } - } - - if tokenDetails == nil { - return nil, fmt.Errorf("unable to process set secret operations, token details are missing") - } - - getAllEnvironmentVariablesRequest := models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, WorkspaceId: projectId} - if tokenDetails.Type == UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - getAllEnvironmentVariablesRequest.UniversalAuthAccessToken = tokenDetails.Token - } - - if tokenDetails.Type == SERVICE_TOKEN_IDENTIFIER { - getAllEnvironmentVariablesRequest.InfisicalToken = tokenDetails.Token - } - - httpClient, err := GetRestyClientWithCustomHeaders() - if err != nil { - return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err) - } - httpClient.SetAuthToken(tokenDetails.Token) - httpClient.SetHeader("Accept", "application/json") - - // pull current secrets - secrets, err := GetAllEnvironmentVariables(getAllEnvironmentVariablesRequest, "") - if err != nil { - return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err) - } - - secretsToCreate := []api.RawSecret{} - secretsToModify := []api.RawSecret{} - secretOperations := []models.SecretSetOperation{} - - sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - - for _, secret := range secrets { - if secret.Type == SECRET_TYPE_PERSONAL { - personalSecretMapByName[secret.Key] = secret - } else { - sharedSecretMapByName[secret.Key] = secret - } - } - - for _, arg := range secretArgs { - splitKeyValueFromArg := strings.SplitN(arg, "=", 2) - if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { - PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again") - } - - if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { - PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again") - } - - // Key and value from argument - key := splitKeyValueFromArg[0] - value := splitKeyValueFromArg[1] - - var existingSecret models.SingleEnvironmentVariable - var doesSecretExist bool - - if secretType == SECRET_TYPE_SHARED { - existingSecret, doesSecretExist = sharedSecretMapByName[key] - } else { - existingSecret, doesSecretExist = personalSecretMapByName[key] - } - - if doesSecretExist { - // case: secret exists in project so it needs to be modified - encryptedSecretDetails := api.RawSecret{ - ID: existingSecret.ID, - SecretValue: value, - SecretKey: key, - Type: existingSecret.Type, - } - - // Only add to modifications if the value is different - if existingSecret.Value != value { - secretsToModify = append(secretsToModify, encryptedSecretDetails) - secretOperations = append(secretOperations, models.SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET VALUE MODIFIED", - }) - } else { - // Current value is same as existing so no change - secretOperations = append(secretOperations, models.SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET VALUE UNCHANGED", - }) - } - - } else { - // case: secret doesn't exist in project so it needs to be created - encryptedSecretDetails := api.RawSecret{ - SecretKey: key, - SecretValue: value, - Type: secretType, - } - secretsToCreate = append(secretsToCreate, encryptedSecretDetails) - secretOperations = append(secretOperations, models.SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET CREATED", - }) - } - } - - for _, secret := range secretsToCreate { - createSecretRequest := api.CreateRawSecretV3Request{ - SecretName: secret.SecretKey, - SecretValue: secret.SecretValue, - Type: secret.Type, - SecretPath: secretsPath, - WorkspaceID: projectId, - Environment: environmentName, - } - - err = api.CallCreateRawSecretsV3(httpClient, createSecretRequest) - if err != nil { - return nil, fmt.Errorf("unable to process new secret creations [err=%v]", err) - } - } - - for _, secret := range secretsToModify { - updateSecretRequest := api.UpdateRawSecretByNameV3Request{ - SecretName: secret.SecretKey, - SecretValue: secret.SecretValue, - SecretPath: secretsPath, - WorkspaceID: projectId, - Environment: environmentName, - Type: secret.Type, - } - - err = api.CallUpdateRawSecretsV3(httpClient, updateSecretRequest) - if err != nil { - return nil, fmt.Errorf("unable to process secret update request [err=%v]", err) - } - } - - return secretOperations, nil - -} diff --git a/cli/packages/util/testdata/infisical-branch-env.json b/cli/packages/util/testdata/infisical-branch-env.json deleted file mode 100644 index 657d68cd9..000000000 --- a/cli/packages/util/testdata/infisical-branch-env.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "workspaceId": "12345678", - "defaultEnvironment": "myDefaultEnv", - "gitBranchToEnvironmentMapping": { - "main": "myMainEnv" - } -} \ No newline at end of file diff --git a/cli/packages/util/testdata/infisical-default-env.json b/cli/packages/util/testdata/infisical-default-env.json deleted file mode 100644 index 63d37ad87..000000000 --- a/cli/packages/util/testdata/infisical-default-env.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "workspaceId": "12345678", - "defaultEnvironment": "myDefaultEnv", - "gitBranchToEnvironmentMapping": null -} \ No newline at end of file diff --git a/cli/packages/util/testdata/infisical-no-matching-branch-env.json b/cli/packages/util/testdata/infisical-no-matching-branch-env.json deleted file mode 100644 index 101dc61d6..000000000 --- a/cli/packages/util/testdata/infisical-no-matching-branch-env.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "workspaceId": "12345678", - "defaultEnvironment": "myDefaultEnv", - "gitBranchToEnvironmentMapping": { - "notmain": "myMainEnv" - } -} \ No newline at end of file diff --git a/cli/packages/util/vault.go b/cli/packages/util/vault.go deleted file mode 100644 index 5907d93fc..000000000 --- a/cli/packages/util/vault.go +++ /dev/null @@ -1,22 +0,0 @@ -package util - -import ( - "fmt" -) - -func GetCurrentVaultBackend() (string, error) { - configFile, err := GetConfigFile() - if err != nil { - return "", fmt.Errorf("getCurrentVaultBackend: unable to get config file [err=%s]", err) - } - - if configFile.VaultBackendType == "" { - return VAULT_BACKEND_AUTO_MODE, nil - } - - if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE { - return VAULT_BACKEND_AUTO_MODE, nil - } - - return configFile.VaultBackendType, nil -} diff --git a/cli/packages/visualize/dynamic_secret_leases.go b/cli/packages/visualize/dynamic_secret_leases.go deleted file mode 100644 index dbb588624..000000000 --- a/cli/packages/visualize/dynamic_secret_leases.go +++ /dev/null @@ -1,39 +0,0 @@ -package visualize - -import infisicalModels "github.com/infisical/go-sdk/packages/models" - -func PrintAllDyamicSecretLeaseCredentials(leaseCredentials map[string]any) { - rows := [][]string{} - for key, value := range leaseCredentials { - if cred, ok := value.(string); ok { - rows = append(rows, []string{key, cred}) - } - } - - headers := []string{"Key", "Value"} - - GenericTable(headers, rows) -} - -func PrintAllDynamicRootCredentials(dynamicRootCredentials []infisicalModels.DynamicSecret) { - rows := [][]string{} - for _, el := range dynamicRootCredentials { - rows = append(rows, []string{el.Name, el.Type, el.DefaultTTL, el.MaxTTL}) - } - - headers := []string{"Name", "Provider", "Default TTL", "Max TTL"} - - GenericTable(headers, rows) -} - -func PrintAllDynamicSecretLeases(dynamicSecretLeases []infisicalModels.DynamicSecretLease) { - rows := [][]string{} - const timeformat = "02-Jan-2006 03:04:05 PM" - for _, el := range dynamicSecretLeases { - rows = append(rows, []string{el.Id, el.ExpireAt.Local().Format(timeformat), el.CreatedAt.Local().Format(timeformat)}) - } - - headers := []string{"ID", "Expire At", "Created At"} - - GenericTable(headers, rows) -} diff --git a/cli/packages/visualize/folders.go b/cli/packages/visualize/folders.go deleted file mode 100644 index 74f053c4f..000000000 --- a/cli/packages/visualize/folders.go +++ /dev/null @@ -1,14 +0,0 @@ -package visualize - -import "github.com/Infisical/infisical-merge/packages/models" - -func PrintAllFoldersDetails(folders []models.SingleFolder, path string) { - rows := [][3]string{} - for _, folder := range folders { - rows = append(rows, [...]string{folder.Name, path, folder.ID}) - } - - headers := [...]string{"FOLDER NAME", "PATH", "FOLDER ID"} - - Table(headers, rows) -} diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go deleted file mode 100644 index 7be41020d..000000000 --- a/cli/packages/visualize/secrets.go +++ /dev/null @@ -1,14 +0,0 @@ -package visualize - -import "github.com/Infisical/infisical-merge/packages/models" - -func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { - rows := [][3]string{} - for _, secret := range secrets { - rows = append(rows, [...]string{secret.Key, secret.Value, secret.Type}) - } - - headers := [...]string{"SECRET NAME", "SECRET VALUE", "SECRET TYPE"} - - Table(headers, rows) -} diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go deleted file mode 100644 index 7fbd24fb8..000000000 --- a/cli/packages/visualize/visualize.go +++ /dev/null @@ -1,134 +0,0 @@ -package visualize - -import ( - "os" - "strings" - - "github.com/jedib0t/go-pretty/table" - "github.com/mattn/go-isatty" - "github.com/muesli/ansi" - "github.com/muesli/reflow/truncate" - "github.com/rs/zerolog/log" - "golang.org/x/term" -) - -type TableOptions struct { - Title string -} - -// func GetDefaultTableOptions() TableOptions{ -// return TableOptions{ -// Title: "", -// } -// } - -const ( - // combined width of the table borders and padding - borderWidths = 10 - // char to indicate that a string has been truncated - ellipsis = "…" -) - -// Given headers and rows, this function will print out a table -func Table(headers [3]string, rows [][3]string) { - // if we're not in a terminal or cygwin terminal, don't truncate the secret value - shouldTruncate := isatty.IsTerminal(os.Stdout.Fd()) - - // This will return an error if we're not in a terminal or - // if the terminal is a cygwin terminal like Git Bash. - width, _, err := term.GetSize(int(os.Stdout.Fd())) - if err != nil { - if shouldTruncate { - log.Error().Msgf("error getting terminal size: %s", err) - } else { - log.Debug().Err(err) - } - } - - longestSecretName, longestSecretType := getLongestValues(append(rows, headers)) - availableWidth := width - longestSecretName - longestSecretType - borderWidths - if availableWidth < 0 { - availableWidth = 0 - } - - t := table.NewWriter() - t.SetOutputMirror(os.Stdout) - t.SetStyle(table.StyleLight) - - // t.SetTitle(tableOptions.Title) - t.Style().Options.DrawBorder = true - t.Style().Options.SeparateHeader = true - t.Style().Options.SeparateColumns = true - - tableHeaders := table.Row{} - for _, header := range headers { - tableHeaders = append(tableHeaders, header) - } - - t.AppendHeader(tableHeaders) - for _, row := range rows { - tableRow := table.Row{} - for i, val := range row { - // only truncate the first column (secret value) - if i == 1 && stringWidth(val) > availableWidth && shouldTruncate { - val = truncate.StringWithTail(val, uint(availableWidth), ellipsis) - } - tableRow = append(tableRow, val) - } - t.AppendRow(tableRow) - } - - t.Render() -} - -// getLongestValues returns the length of the longest secret name and type from all rows (including the header). -func getLongestValues(rows [][3]string) (longestSecretName, longestSecretType int) { - for _, row := range rows { - if len(row[0]) > longestSecretName { - longestSecretName = stringWidth(row[0]) - } - if len(row[2]) > longestSecretType { - longestSecretType = stringWidth(row[2]) - } - } - return -} - -func GenericTable(headers []string, rows [][]string) { - t := table.NewWriter() - t.SetOutputMirror(os.Stdout) - t.SetStyle(table.StyleLight) - - // t.SetTitle(tableOptions.Title) - t.Style().Options.DrawBorder = true - t.Style().Options.SeparateHeader = true - t.Style().Options.SeparateColumns = true - - tableHeaders := table.Row{} - for _, header := range headers { - tableHeaders = append(tableHeaders, header) - } - - t.AppendHeader(tableHeaders) - for _, row := range rows { - tableRow := table.Row{} - for _, val := range row { - tableRow = append(tableRow, val) - } - t.AppendRow(tableRow) - } - - t.Render() -} - -// stringWidth returns the width of a string. -// ANSI escape sequences are ignored and double-width characters are handled correctly. -func stringWidth(str string) (width int) { - for _, l := range strings.Split(str, "\n") { - w := ansi.PrintableRuneWidth(l) - if w > width { - width = w - } - } - return width -} diff --git a/cli/scripts/completions.sh b/cli/scripts/completions.sh deleted file mode 100755 index 6e69a1508..000000000 --- a/cli/scripts/completions.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -e -rm -rf completions -mkdir completions -cd cli -for sh in bash zsh fish; do - go run . completion "$sh" > "../completions/infisical.$sh" -done \ No newline at end of file diff --git a/cli/scripts/export_test_env.sh b/cli/scripts/export_test_env.sh deleted file mode 100644 index 0b242281d..000000000 --- a/cli/scripts/export_test_env.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -TEST_ENV_FILE=".test.env" - -# Check if the .env file exists -if [ ! -f "$TEST_ENV_FILE" ]; then - echo "$TEST_ENV_FILE does not exist." - exit 1 -fi - -# Export the variables -while IFS= read -r line -do - # Skip empty lines and lines starting with # - if [[ -z "$line" || "$line" =~ ^\# ]]; then - continue - fi - # Read the key-value pair - IFS='=' read -r key value <<< "$line" - eval export $key=\$value -done < "$TEST_ENV_FILE" - -echo "Test environment variables set." diff --git a/cli/scripts/install.sh b/cli/scripts/install.sh deleted file mode 100755 index a6dd4a4ec..000000000 --- a/cli/scripts/install.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash - -PLATFORM= -ARCH= -TEMP_DOWNLOAD_FOLDER= - -function delete_temp_install_folder() -{ - $(rm -rf $TEMP_DOWNLOAD_FOLDER 2> /dev/null) -} - -# platform -case "$(uname -s)" in - Linux) PLATFORM='linux';; - Darwin) PLATFORM='darwin';; - CYGWIN*|MINGW*|MSYS*) PLATFORM='windows';; - FreeBSD) PLATFORM='freebsd';; - *) - echo "Your platform doesn't seem to be of type darwin, linux or windows" - echo "Your architecture is $(uname -m) and your platform is $(uname -s)" - exit 1 - ;; -esac - -# architecture -if [[ "$(uname -m)" == 'x86_64' || "$(uname -m)" == "amd64" ]]; then - ARCH="amd64" -elif [[ "$(uname -m)" == armv5* ]]; then - ARCH="armv5" -elif [[ "$(uname -m)" == armv6* ]]; then - ARCH="armv6" -elif [[ "$(uname -m)" == armv7* ]]; then - ARCH="armv7" -elif [[ "$(uname -m)" == 'arm64' || "$(uname -m)" == 'aarch64' ]]; then - ARCH="arm64" -elif [[ "$(uname -m)" == "i386" || "$(uname -m)" == "i686" ]]; then - ARCH="i386" -else - echo >&2 "Your architecture doesn't seem to supported. Your architecture is $(uname -m) and your platform is $(uname -s)" - exit 1 -fi - -# Credit https://stackoverflow.com/questions/20010199/how-to-determine-if-a-process-runs-inside-lxc-docker -if [[ "$(cat /proc/1/cgroup 2> /dev/null | grep docker | wc -l)" > 0 ]] || [ -f /.dockerenv ]; then - IS_RUNNING_IN_DOCKER=true -else - IS_RUNNING_IN_DOCKER=false -fi - -# example: v0.0.98 -LATEST_RELEASE_VERSION=$(curl -s "https://api.github.com/repos/Infisical/infisical/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') - -# example: 0.0.98 -NUMERIC_RELEASE_VERSION="${LATEST_RELEASE_VERSION:1}" - -DOWNLOAD_LINK="https://github.com/Infisical/infisical/releases/download/${LATEST_RELEASE_VERSION}/infisical_${NUMERIC_RELEASE_VERSION}_${PLATFORM}_${ARCH}.tar.gz" - -CHECK_IF_BINARY_EXISTS=$(curl -s -o -L /dev/null -w "%{http_code}" ${DOWNLOAD_LINK}) -if [[ $CHECK_IF_BINARY_EXISTS == "000Not Found404" ]]; then - echo "Looks like we do not yet have a binary for this architecture and platform." - echo "Your architecture is $(uname -m) and your platform is $(uname -s)" - exit 1 -fi - -# make temp install folder -mkdir -p infisical_temp_download_folder - -cd infisical_temp_download_folder - -TEMP_DOWNLOAD_FOLDER=$(pwd) - -# download latest cli -curl -L -o infisical-binary.tar.gz ${DOWNLOAD_LINK} - -# open up the tar file -tar zxf infisical-binary.tar.gz - -if [ "$PLATFORM" == "darwin" ] || [ $RUNNING_IN_DOCKER ] ; then - if [[ -d /usr/local/bin ]]; then - mv infisical /usr/local/bin/ - echo "Infisical CLI ${LATEST_RELEASE_VERSION} has been installed in /usr/local/bin." - else - echo >&2 "Error: /usr/local/bin does not exist. You must create it before reinstalling" - delete_temp_install_folder - exit 1 - fi -elif [ "$PLATFORM" == "windows" ]; then - mkdir $HOME/bin 2> /dev/null - mv infisical.exe $HOME/bin/ - echo "Infisical CLI ${LATEST_RELEASE_VERSION} has been installed in $HOME/bin" - echo "Please add $HOME/bin to your system PATH" -else - sudo mv infisical /usr/local/bin/ - echo "Infisical CLI ${LATEST_RELEASE_VERSION} has been installed in /usr/local/bin." -fi - -delete_temp_install_folder \ No newline at end of file diff --git a/cli/scripts/manpages.sh b/cli/scripts/manpages.sh deleted file mode 100755 index db7e5c1b5..000000000 --- a/cli/scripts/manpages.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -e -rm -rf manpages -mkdir manpages -cd cli -go run . man | gzip -c > "../manpages/infisical.1.gz" \ No newline at end of file diff --git a/cli/scripts/setup.deb.sh b/cli/scripts/setup.deb.sh deleted file mode 100644 index ef24bcadc..000000000 --- a/cli/scripts/setup.deb.sh +++ /dev/null @@ -1,551 +0,0 @@ -#!/usr/bin/env bash -# -# The core commands execute start from the "MAIN" section below. -# - -test -z "$BASH_SOURCE" && { - self="sudo -E bash" - prefix=" |" -} || { - self=$(readlink -f ${BASH_SOURCE:-$0}) - prefix="" -} - -tmp_log=$(mktemp .s3_setup_XXXXXXXXX) - -# Environment variables that can be set -PKG_URL=${PKG_URL:-"https://artifacts-cli.infisical.com"} -PKG_PATH=${PKG_PATH:-"deb"} -PACKAGE_NAME=${PACKAGE_NAME:-"infisical"} -GPG_KEY_URL=${GPG_KEY_URL:-"${PKG_URL}/infisical.gpg"} - -colours=$(tput colors 2>/dev/null || echo "256") -no_colour="\e[39;49m" -green_colour="\e[32m" -red_colour="\e[41;97m" -bold="\e[1m" -reset="\e[0m" -use_colours=$(test -n "$colours" && test $colours -ge 8 && echo "yes") -test "$use_colours" == "yes" || { - no_colour="" - green_colour="" - red_colour="" - bold="" - reset="" -} - -example_name="Ubuntu/Focal (20.04)" -example_distro="ubuntu" -example_codename="focal" -example_version="20.04" - -function echo_helptext { - local help_text="$*" - echo " ^^^^: ... $help_text" -} - -function die { - local text="$@" - test ! -z "$text" && { - echo_helptext "$text" 1>&2 - } - - local prefix="${red_colour} !!!!${no_colour}" - - echo -e "$prefix: Oh no, your setup failed! :-( ... But we might be able to help. :-)" - echo -e "$prefix: " - echo -e "$prefix: ${bold}Please check your S3 bucket configuration and try again.${reset}" - echo -e "$prefix: " - - test -f "$tmp_log" && { - local n=20 - echo -e "$prefix: Last $n log lines from $tmp_log (might not be errors, nor even relevant):" - echo -e "$prefix:" - check_tool_silent "xargs" && { - check_tool_silent "fmt" && { - tail -n $n $tmp_log | fmt -t | xargs -Ilog echo -e "$prefix: > log" - } || { - tail -n $n $tmp_log | xargs -Ilog echo -e "$prefix: > log" - } - } || { - echo - tail -n $n $tmp_log - } - } - exit 1 -} - -function echo_colour { - local colour="${1:-"no"}_colour"; shift - echo -e "${!colour}$@${no_colour}" -} - -function echo_green_or_red { - local rc="$1" - local good="${2:-YES}" - local bad="${3:-NO}" - - test "$rc" -eq 0 && { - echo_colour "green" "$good" - } || { - echo_colour "red" "$bad" - } - return $rc -} - -function echo_clearline { - local rc="$?" - echo -e -n "\033[1K\r" - return $rc -} - -function echo_status { - local rc="$1" - local good="$2" - local bad="$3" - local text="$4" - local help_text="$5" - local newline=$(test "$6" != "no" && echo "\n" || echo "") - local status_text=$(echo_green_or_red "$rc" "$good" "$bad") - - echo_clearline - local width=$(test "$use_colours" == "yes" && echo "16" || echo "5") - printf "%${width}s %s${newline}" "${status_text}:" "$text" - test $rc -ne 0 && test ! -z "$help_text" && { - echo_helptext "$help_text" - echo - } - - return $rc -} - -function echo_running { - local rc=$? - local text="$1" - echo_status 0 " RUN" " RUN" "$text" "" "no" - return $rc -} - -function echo_okfail_rc { - local rc=$1 - local text="$2" - local help_text="$3" - echo_clearline - echo_status $rc " OK" " NOPE" "$text" "$help_text" - return $rc -} - -function echo_okfail { - echo_okfail_rc $? "$@" - return $? -} - -function check_tool_silent { - local tool=${1} - command -v $tool &>/dev/null || which $tool &>/dev/null - return $? -} - -function check_tool { - local tool=${1} - local optional=${2:-false} - local required_text="optional" - if ! $optional; then required_text="required"; fi - local text="Checking for $required_text executable '$tool' ..." - echo_running "$text" - check_tool_silent "$tool" - echo_okfail "$text" || { - if ! $optional; then - die "$tool is not installed, but is required by this script." - fi - return 1 - } - return 0 -} - -function cleanup { - echo - rm -rf $tmp_log -} - -function shutdown { - echo_colour "red" " !!!!: Operation cancelled by user!" - exit 2 -} - -function check_os { - test ! -z "$distro" && test ! -z "${version}${codename}" - return $? -} - -function detect_os_system { - check_os && return 0 - echo_running "$text" - local text="Detecting your OS distribution and release using system methods ..." - - local tool_rc=1 - test -f '/etc/os-release' && { - . /etc/os-release - distro=${distro:-$ID} - codename=${codename:-$VERSION_CODENAME} - codename=${codename:-$(echo $VERSION | cut -d '(' -f 2 | cut -d ')' -f 1)} - version=${version:-$VERSION_ID} - - test -z "${version}${codename}" && test -f '/etc/debian_version' && { - # Workaround for Debian unstable releases; get the codename from debian_version - codename=$(cat /etc/debian_version | cut -d '/' -f1) - } - - tool_rc=0 - } - - check_os - local rc=$? - echo_okfail_rc $rc "$text" - - test $tool_rc -eq 0 && { - report_os_expanded - } - - return $rc -} - -function report_os_attribute { - local name=$1 - local value=$2 - local coloured="" - echo -n "$name=" - test -z "$value" && { - echo -e -n "${red_colour}${no_colour} " - } || { - echo -e -n "${green_colour}${value}${no_colour} " - } -} - -function report_os_expanded { - echo_helptext "Detected/provided for your OS/distribution, version and architecture:" - echo " >>>>:" - report_os_values -} - -function report_os_values { - echo -n " >>>>: ... " - report_os_attribute "distro" $distro - report_os_attribute "codename" "stable (fixed)" - report_os_attribute "arch" $arch - echo - echo " >>>>:" -} - -function detect_os_legacy_python { - check_os && return 0 - - local text="Detecting your OS distribution and release using legacy python ..." - echo_running "$text" - - IFS='' read -r -d '' script <<-'EOF' -from __future__ import unicode_literals, print_function -import platform; -info = platform.linux_distribution() or ('', '', ''); -for key, value in zip(('distro', 'version', 'codename'), info): - print("local guess_%s=\"%s\"\n" % (key, value.lower().replace(' ', ''))); -EOF - - local tool_rc=1 - check_tool_silent "python" && { - eval $(python -c "$script") - distro=${distro:-$guess_distro} - codename=${codename:-$guess_codename} - version=${version:-$guess_version} - tool_rc=$? - } - - check_os - local rc=$? - echo_okfail_rc $rc "$text" - - check_tool_silent "python" || { - echo_helptext "Python isn't available, so skipping detection method (hint: install python)" - } - - test $tool_rc -eq 0 && { - report_os - } - - return $rc -} - -function detect_os_modern_python { - check_os && return 0 - - check_tool_silent "python" && { - local text="Ensuring python-pip is installed ..." - echo_running "$text" - check_tool_silent "pip" - echo_okfail "$text" || { - local text="Checking if pip can be bootstrapped without get-pip ..." - echo_running "$text" - python -m ensurepip --default-pip &>$tmp_log - echo_okfail "$text" || { - local text="Installing pip via get-pip bootstrap ..." - echo_running "$text" - curl -1sLf https://bootstrap.pypa.io/get-pip.py 2>$tmp/log | python &>$tmp_log - echo_okfail "$text" || die "Failed to install pip!" - } - } - - local text="Installing 'distro' python library ..." - echo_running "$text" - python -c 'import distro' &>$tmp_log || python -m pip install distro &>$tmp_log - echo_okfail "$text" || die "Failed to install required 'distro' python library!" - } - - IFS='' read -r -d '' script <<-'EOF' -from __future__ import unicode_literals, print_function -import distro; -info = distro.linux_distribution(full_distribution_name=False) or ('', '', ''); -for key, value in zip(('distro', 'version', 'codename'), info): - print("local guess_%s=\"%s\"\n" % (key, value.lower().replace(' ', ''))); -EOF - - local text="Detecting your OS distribution and release using modern python ..." - echo_running "$text" - - local tool_rc=1 - check_tool_silent "python" && { - eval $(python -c "$script") - distro=${distro:-$guess_distro} - codename=${codename:-$guess_codename} - version=${version:-$guess_version} - tool_rc=$? - } - - check_os - local rc=$? - echo_okfail_rc $rc "$text" - - check_tool_silent "python" || { - echo_helptext "Python isn't available, so skipping detection method (hint: install python)" - } - - test $tool_rc -eq 0 && { - report_os_expanded - } - - return $rc -} - -function detect_os { - # Backwards compat for old distribution parameter names - distro=${distro:-$os} - - # Always use "stable" as the codename - codename="stable" - - arch=${arch:-$(arch || uname -m)} - - # Only detect OS if not manually specified - if [ -z "$distro" ]; then - detect_os_system || - detect_os_legacy_python || - detect_os_modern_python - fi - - # Always ensure we have a distro - (test -z "$distro") && { - echo_okfail_rc "1" "Unable to detect your OS distribution!" - cat <>>>: - >>>>: The 'distro' value is required. Without it, the install script - >>>>: cannot retrieve the correct configuration for this system. - >>>>: - >>>>: You can force this script to use a particular value by specifying distro - >>>>: via environment variable. E.g., to specify a distro - >>>>: such as $example_name, use the following: - >>>>: - >>>>: $prefix distro=$example_distro $self - >>>>: -EOF - die - } -} - -function create_repo_config { - if [ -z "$PKG_PATH" ]; then - repo_url="${PKG_URL}" - else - repo_url="${PKG_URL}/${PKG_PATH}" - fi - - # Create configuration with GPG key verification - local gpg_keyring_path="/usr/share/keyrings/${PACKAGE_NAME}-archive-keyring.gpg" - local apt_conf=$(cat <>>>: - >>>>: It looks like we can't access the GPG key at ${GPG_KEY_URL} - >>>>: -EOF - die - } -} - -function check_dpkg_tool { - local tool=${1} - local required=${2:-true} - local install=${3:-true} - - local text="Checking for apt dependency '$tool' ..." - echo_running "$text" - dpkg -l | grep "$tool\>" &>$tmp_log - echo_okfail "$text" || { - if $install; then - test "$apt_updated" == "yes" || update_apt - local text="Attempting to install '$tool' ..." - echo_running "$text" - apt-get install -y "$tool" &>$tmp_log - echo_okfail "$text" || { - if $required; then - die "Could not install '$tool', check your permissions, etc." - fi - } - else { - if $required; then - die "$tool is not installed, but is required by this script." - fi - } - fi - } - return 0 -} - -function update_apt { - local text="Updating apt repository metadata cache ..." - local tmp_log=$(mktemp .s3_deb_output_XXXXXXXXX.log) - echo_running "$text" - apt-get update &>$tmp_log - echo_okfail "$text" || { - echo_colour "red" "Failed to update via apt-get update" - cat $tmp_log - rm -rf $tmp_log - die "Failed to update via apt-get update - Context above (maybe no packages?)." - } - rm -rf $tmp_log - apt_updated="yes" -} - -function install_apt_prereqs { - # Debian-archive-keyring has to be installed for apt-transport-https. - test "${distro}" == "debian" && { - check_dpkg_tool "debian-keyring" - check_dpkg_tool "debian-archive-keyring" - } - - check_dpkg_tool "apt-transport-https" - check_dpkg_tool "ca-certificates" false - check_dpkg_tool "gnupg" -} - -function import_gpg_key { - local text="Importing '$PACKAGE_NAME' repository GPG key from S3 ..." - echo_running "$text" - - local gpg_keyring_path="/usr/share/keyrings/${PACKAGE_NAME}-archive-keyring.gpg" - - # Check if GPG key is accessible - check_gpg_key - - # Download and import GPG key - curl -1sLf "${GPG_KEY_URL}" | gpg --dearmor > $gpg_keyring_path - chmod 644 $gpg_keyring_path - - # Check for older apt versions that don't support signed-by - local signed_by_version="1.1" - local detected_version=$(dpkg -s apt | grep Version | cut -d' ' -f2) - - if [ "$(printf "%s\n" $detected_version $signed_by_version | sort -V | head -n 1)" != "$signed_by_version" ]; then - echo_helptext "Detected older apt version without signed-by support. Copying key to trusted.gpg.d." - cp ${gpg_keyring_path} /etc/apt/trusted.gpg.d/${PACKAGE_NAME}.gpg - chmod 644 /etc/apt/trusted.gpg.d/${PACKAGE_NAME}.gpg - fi - - echo_okfail "$text" || die "Could not import the GPG key for this repository" -} - -function setup_repository { - local repo_path="/etc/apt/sources.list.d/${PACKAGE_NAME}.list" - - local text="Installing '$PACKAGE_NAME' repository via apt ..." - echo_running "$text" - create_repo_config > "$repo_path" - chmod 644 $repo_path - echo_okfail "$text" || die "Could not install the repository, do you have permissions?" -} - -function usage () { - cat <] [message="Environment with slug 'invalid-env' in project with ID bef697d4-849b-4a75-b284-0922f87f8ba2 not found"] - - -If this issue continues, get support at https://infisical.com/slack diff --git a/cli/test/.snapshots/test-TestUserAuth_SecretsGetAll b/cli/test/.snapshots/test-TestUserAuth_SecretsGetAll deleted file mode 100644 index 260607e97..000000000 --- a/cli/test/.snapshots/test-TestUserAuth_SecretsGetAll +++ /dev/null @@ -1,7 +0,0 @@ -┌───────────────┬──────────────┬─────────────┐ -│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ -├───────────────┼──────────────┼─────────────┤ -│ TEST-SECRET-1 │ test-value-1 │ shared │ -│ TEST-SECRET-2 │ test-value-2 │ shared │ -│ TEST-SECRET-3 │ test-value-3 │ shared │ -└───────────────┴──────────────┴─────────────┘ diff --git a/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection b/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection deleted file mode 100644 index 2ca9d13ad..000000000 --- a/cli/test/.snapshots/test-testUserAuth_SecretsGetAllWithoutConnection +++ /dev/null @@ -1,8 +0,0 @@ -Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug -┌───────────────┬──────────────┬─────────────┐ -│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ -├───────────────┼──────────────┼─────────────┤ -│ TEST-SECRET-1 │ test-value-1 │ shared │ -│ TEST-SECRET-2 │ test-value-2 │ shared │ -│ TEST-SECRET-3 │ test-value-3 │ shared │ -└───────────────┴──────────────┴─────────────┘ diff --git a/cli/test/export_test.go b/cli/test/export_test.go deleted file mode 100644 index c44bf20af..000000000 --- a/cli/test/export_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package tests - -import ( - "testing" - - "github.com/bradleyjkemp/cupaloy/v2" -) - -func TestUniversalAuth_ExportSecretsWithImports(t *testing.T) { - MachineIdentityLoginCmd(t) - - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestServiceToken_ExportSecretsWithImports(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_ExportSecretsWithoutImports(t *testing.T) { - MachineIdentityLoginCmd(t) - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestServiceToken_ExportSecretsWithoutImports(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} diff --git a/cli/test/helper.go b/cli/test/helper.go deleted file mode 100644 index 74e56237a..000000000 --- a/cli/test/helper.go +++ /dev/null @@ -1,107 +0,0 @@ -package tests - -import ( - "encoding/json" - "fmt" - "log" - "os" - "os/exec" - "regexp" - "strings" -) - -const ( - CLI_NAME = "infisical-merge" -) - -var ( - FORMATTED_CLI_NAME = fmt.Sprintf("./%s", CLI_NAME) -) - -type Credentials struct { - ClientID string - ClientSecret string - UAAccessToken string - ServiceToken string - ProjectID string - EnvSlug string - UserEmail string - UserPassword string -} - -var creds = Credentials{ - UAAccessToken: "", - ClientID: os.Getenv("CLI_TESTS_UA_CLIENT_ID"), - ClientSecret: os.Getenv("CLI_TESTS_UA_CLIENT_SECRET"), - ServiceToken: os.Getenv("CLI_TESTS_SERVICE_TOKEN"), - ProjectID: os.Getenv("CLI_TESTS_PROJECT_ID"), - EnvSlug: os.Getenv("CLI_TESTS_ENV_SLUG"), - UserEmail: os.Getenv("CLI_TESTS_USER_EMAIL"), - UserPassword: os.Getenv("CLI_TESTS_USER_PASSWORD"), -} - -func ExecuteCliCommand(command string, args ...string) (string, error) { - cmd := exec.Command(command, args...) - output, err := cmd.CombinedOutput() - - if err != nil { - fmt.Println(fmt.Sprint(err) + ": " + FilterRequestID(strings.TrimSpace(string(output)))) - return FilterRequestID(strings.TrimSpace(string(output))), err - } - return FilterRequestID(strings.TrimSpace(string(output))), nil -} - -func SetupCli() { - - if creds.ClientID == "" || creds.ClientSecret == "" || creds.ServiceToken == "" || creds.ProjectID == "" || creds.EnvSlug == "" { - panic("Missing required environment variables") - } - - // check if the CLI is already built, if not build it - alreadyBuilt := false - if _, err := os.Stat(FORMATTED_CLI_NAME); err == nil { - alreadyBuilt = true - } - - if !alreadyBuilt { - if err := exec.Command("go", "build", "../.").Run(); err != nil { - log.Fatal(err) - } - } - -} - -func FilterRequestID(input string) string { - requestIDPattern := regexp.MustCompile(`\[request-id=[^\]]+\]`) - reqIDPattern := regexp.MustCompile(`\[reqId=[^\]]+\]`) - input = requestIDPattern.ReplaceAllString(input, "[request-id=]") - input = reqIDPattern.ReplaceAllString(input, "[reqId=]") - - start := strings.Index(input, "{") - end := strings.LastIndex(input, "}") + 1 - - if start == -1 || end == -1 { - return input - } - - jsonPart := input[:start] // Pre-JSON content - - // Parse the JSON object - var errorObj map[string]interface{} - if err := json.Unmarshal([]byte(input[start:end]), &errorObj); err != nil { - return input - } - - // Remove requestId field - delete(errorObj, "requestId") - delete(errorObj, "reqId") - - // Convert back to JSON - filtered, err := json.Marshal(errorObj) - if err != nil { - return input - } - - // Reconstruct the full string - return jsonPart + string(filtered) + input[end:] -} diff --git a/cli/test/login_test.go b/cli/test/login_test.go deleted file mode 100644 index 71273a3ec..000000000 --- a/cli/test/login_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package tests - -import ( - "log" - "os/exec" - "strings" - "testing" - - "github.com/creack/pty" - "github.com/stretchr/testify/assert" -) - -func UserInitCmd() { - c := exec.Command(FORMATTED_CLI_NAME, "init") - ptmx, err := pty.Start(c) - if err != nil { - log.Fatalf("error running CLI command: %v", err) - } - defer func() { _ = ptmx.Close() }() - - stepChan := make(chan int, 10) - - go func() { - buf := make([]byte, 1024) - step := -1 - for { - n, err := ptmx.Read(buf) - if n > 0 { - terminalOut := string(buf) - if strings.Contains(terminalOut, "Which Infisical organization would you like to select a project from?") && step < 0 { - step += 1 - stepChan <- step - } else if strings.Contains(terminalOut, "Which of your Infisical projects would you like to connect this project to?") && step < 1 { - step += 1; - stepChan <- step - } - } - if err != nil { - close(stepChan) - return - } - } - }() - - for i := range stepChan { - switch i { - case 0: - ptmx.Write([]byte("\n")) - case 1: - ptmx.Write([]byte("\n")) - } - } -} - -func UserLoginCmd() { - // set vault to file because CI has no keyring - vaultCmd := exec.Command(FORMATTED_CLI_NAME, "vault", "set", "file") - _, err := vaultCmd.Output() - if err != nil { - log.Fatalf("error setting vault: %v", err) - } - - // Start programmatic interaction with CLI - c := exec.Command(FORMATTED_CLI_NAME, "login", "--interactive") - ptmx, err := pty.Start(c) - if err != nil { - log.Fatalf("error running CLI command: %v", err) - } - defer func() { _ = ptmx.Close() }() - - stepChan := make(chan int, 10) - - go func() { - buf := make([]byte, 1024) - step := -1 - for { - n, err := ptmx.Read(buf) - if n > 0 { - terminalOut := string(buf) - if strings.Contains(terminalOut, "Infisical Cloud") && step < 0 { - step += 1; - stepChan <- step - } else if strings.Contains(terminalOut, "Email") && step < 1 { - step += 1; - stepChan <- step - } else if strings.Contains(terminalOut, "Password") && step < 2 { - step += 1; - stepChan <- step - } else if strings.Contains(terminalOut, "Infisical organization") && step < 3 { - step += 1; - stepChan <- step - } else if strings.Contains(terminalOut, "Enter passphrase") && step < 4 { - step += 1; - stepChan <- step - } - } - if err != nil { - close(stepChan) - return - } - } - }() - - for i := range stepChan { - switch i { - case 0: - ptmx.Write([]byte("\n")) - case 1: - ptmx.Write([]byte(creds.UserEmail)) - ptmx.Write([]byte("\n")) - case 2: - ptmx.Write([]byte(creds.UserPassword)) - ptmx.Write([]byte("\n")) - case 3: - ptmx.Write([]byte("\n")) - } - } - -} - -func MachineIdentityLoginCmd(t *testing.T) { - if creds.UAAccessToken != "" { - return - } - - jwtPattern := `^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$` - - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "login", "--method=universal-auth", "--client-id", creds.ClientID, "--client-secret", creds.ClientSecret, "--plain", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - assert.Regexp(t, jwtPattern, output) - - creds.UAAccessToken = output - - // We can't use snapshot testing here because the output will be different every time -} diff --git a/cli/test/main_test.go b/cli/test/main_test.go deleted file mode 100644 index e14893aec..000000000 --- a/cli/test/main_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package tests - -import ( - "fmt" - "os" - "testing" -) - -func TestMain(m *testing.M) { - // Setup - fmt.Println("Setting up CLI...") - SetupCli() - fmt.Println("Performing user login...") - UserLoginCmd() - fmt.Println("Performing infisical init...") - UserInitCmd() - - // Run the tests - code := m.Run() - - // Exit - os.Exit(code) -} diff --git a/cli/test/run_test.go b/cli/test/run_test.go deleted file mode 100644 index d2c6021cc..000000000 --- a/cli/test/run_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package tests - -import ( - "bytes" - "testing" - - "github.com/bradleyjkemp/cupaloy/v2" -) - -func TestServiceToken_RunCmdRecursiveAndImports(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent", "--", "echo", "hello world") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - output = string(bytes.Split([]byte(output), []byte("INF"))[1]) - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} -func TestServiceToken_RunCmdWithImports(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--", "echo", "hello world") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - output = string(bytes.Split([]byte(output), []byte("INF"))[1]) - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_RunCmdRecursiveAndImports(t *testing.T) { - MachineIdentityLoginCmd(t) - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent", "--", "echo", "hello world") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - output = string(bytes.Split([]byte(output), []byte("INF"))[1]) - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_RunCmdWithImports(t *testing.T) { - MachineIdentityLoginCmd(t) - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--", "echo", "hello world") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // remove the first few characters from the output because we don't care about the time, and it will change every time - output = string(bytes.Split([]byte(output), []byte("INF"))[1]) - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_RunCmdWithoutImports(t *testing.T) { - MachineIdentityLoginCmd(t) - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false", "--", "echo", "hello world") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - output = string(bytes.Split([]byte(output), []byte("INF"))[1]) - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestServiceToken_RunCmdWithoutImports(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false", "--", "echo", "hello world") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Remove everything before "INF" because it's not relevant to the test - output = string(bytes.Split([]byte(output), []byte("INF"))[1]) - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} diff --git a/cli/test/secrets_by_name_test.go b/cli/test/secrets_by_name_test.go deleted file mode 100644 index 26a8314bb..000000000 --- a/cli/test/secrets_by_name_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package tests - -import ( - "testing" - - "github.com/bradleyjkemp/cupaloy/v2" -) - -func TestServiceToken_GetSecretsByNameRecursive(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestServiceToken_GetSecretsByNameWithNotFoundSecret(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "DOES-NOT-EXIST", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestServiceToken_GetSecretsByNameWithImports(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "STAGING-SECRET-2", "FOLDER-SECRET-1", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_GetSecretsByNameRecursive(t *testing.T) { - MachineIdentityLoginCmd(t) - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_GetSecretsByNameWithNotFoundSecret(t *testing.T) { - MachineIdentityLoginCmd(t) - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "DOES-NOT-EXIST", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_GetSecretsByNameWithImports(t *testing.T) { - MachineIdentityLoginCmd(t) - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "STAGING-SECRET-2", "FOLDER-SECRET-1", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} diff --git a/cli/test/secrets_test.go b/cli/test/secrets_test.go deleted file mode 100644 index f7f0f13ff..000000000 --- a/cli/test/secrets_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package tests - -import ( - "testing" - - "github.com/bradleyjkemp/cupaloy/v2" -) - -func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmd(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) { - MachineIdentityLoginCmd(t) - - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmd(t *testing.T) { - MachineIdentityLoginCmd(t) - - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent") - - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } -} - -func TestUniversalAuth_SecretsGetWrongEnvironment(t *testing.T) { - MachineIdentityLoginCmd(t) - - output, _ := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", "invalid-env", "--recursive", "--silent") - - // Use cupaloy to snapshot test the output - err := cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } - -} - -func TestUserAuth_SecretsGetAll(t *testing.T) { - output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent") - if err != nil { - t.Fatalf("error running CLI command: %v", err) - } - - // Use cupaloy to snapshot test the output - err = cupaloy.Snapshot(output) - if err != nil { - t.Fatalf("snapshot failed: %v", err) - } - - // explicitly called here because it should happen directly after successful secretsGetAll - // testUserAuth_SecretsGetAllWithoutConnection(t) -} - -// disabled for the time being -// func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) { -// originalConfigFile, err := util.GetConfigFile() -// if err != nil { -// t.Fatalf("error getting config file") -// } -// newConfigFile := originalConfigFile - -// // set it to a URL that will always be unreachable -// newConfigFile.LoggedInUserDomain = "http://localhost:4999" -// util.WriteConfigFile(&newConfigFile) - -// // restore config file -// defer util.WriteConfigFile(&originalConfigFile) - -// output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent") -// if err != nil { -// t.Fatalf("error running CLI command: %v", err) -// } - -// // Use cupaloy to snapshot test the output -// err = cupaloy.Snapshot(output) -// if err != nil { -// t.Fatalf("snapshot failed: %v", err) -// } -// } diff --git a/cli/testdata/baseline/baseline.csv b/cli/testdata/baseline/baseline.csv deleted file mode 100644 index d3f953727..000000000 --- a/cli/testdata/baseline/baseline.csv +++ /dev/null @@ -1,2 +0,0 @@ -RuleID,Commit,File,Secret,Match,StartLine,EndLine,StartColumn,EndColumn,Author,Message,Date,Email,Fingerprint -1,b,c,f,s,m,s,e,s,e,a,m,f,r,f \ No newline at end of file diff --git a/cli/testdata/baseline/baseline.json b/cli/testdata/baseline/baseline.json deleted file mode 100644 index 3a4c5427f..000000000 --- a/cli/testdata/baseline/baseline.json +++ /dev/null @@ -1,40 +0,0 @@ -[ - { - "Description": "PyPI upload token", - "StartLine": 32, - "EndLine": 32, - "StartColumn": 21, - "EndColumn": 106, - "Match": "************************", - "Secret": "************************", - "File": "detect/detect_test.go", - "Commit": "9326f35380636bcbe61e94b0584d1618c4b5c2c2", - "Entropy": 1.9606875, - "Author": "****", - "Email": "****", - "Date": "2022-03-07T14:33:06Z", - "Message": "Escape - character in regex character groups (#802)\n\n* fix char escape\n\n* add test\n\n* fix verbosity in make test", - "Tags": [], - "RuleID": "pypi-upload-token", - "Fingerprint": "9326f35380636bcbe61e94b0584d1618c4b5c2c2:detect/detect_test.go:pypi-upload-token:32" - }, - { - "Description": "PyPI upload token", - "StartLine": 33, - "EndLine": 33, - "StartColumn": 21, - "EndColumn": 106, - "Match": "************************", - "Secret": "************************", - "File": "detect/detect_test.go", - "Commit": "9326f35380636bcbe61e94b0584d1618c4b5c2c2", - "Entropy": 1.9606875, - "Author": "****", - "Email": "****", - "Date": "2022-03-07T14:33:06Z", - "Message": "Escape - character in regex character groups (#802)\n\n* fix char escape\n\n* add test\n\n* fix verbosity in make test", - "Tags": [], - "RuleID": "pypi-upload-token", - "Fingerprint": "9326f35380636bcbe61e94b0584d1618c4b5c2c2:detect/detect_test.go:pypi-upload-token:33" - } -] diff --git a/cli/testdata/baseline/baseline.sarif b/cli/testdata/baseline/baseline.sarif deleted file mode 100644 index b2f84890a..000000000 --- a/cli/testdata/baseline/baseline.sarif +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/sarif-2.1.0.json", - "version": "2.1.0", - "runs": [ - ] -} diff --git a/cli/testdata/config/allow_aws_re.toml b/cli/testdata/config/allow_aws_re.toml deleted file mode 100644 index 2e2d4f278..000000000 --- a/cli/testdata/config/allow_aws_re.toml +++ /dev/null @@ -1,9 +0,0 @@ -title = "simple config with allowlist for aws" - -[[rules]] - description = "AWS Access Key" - id = "aws-access-key" - regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' - tags = ["key", "AWS"] - [rules.allowlist] - regexes = ['''AKIALALEMEL33243OLIA'''] diff --git a/cli/testdata/config/allow_commit.toml b/cli/testdata/config/allow_commit.toml deleted file mode 100644 index ee8fefd83..000000000 --- a/cli/testdata/config/allow_commit.toml +++ /dev/null @@ -1,9 +0,0 @@ -title = "simple config with allowlist for a specific commit" - -[[rules]] - description = "AWS Access Key" - id = "aws-access-key" - regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' - tags = ["key", "AWS"] - [rules.allowlist] - commits = ['''allowthiscommit'''] diff --git a/cli/testdata/config/allow_global_aws_re.toml b/cli/testdata/config/allow_global_aws_re.toml deleted file mode 100644 index 7b7c3eeed..000000000 --- a/cli/testdata/config/allow_global_aws_re.toml +++ /dev/null @@ -1,8 +0,0 @@ -[[rules]] - description = "AWS Access Key" - id = "aws-access-key" - regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' - tags = ["key", "AWS"] - -[allowlist] - regexes = ['''AKIALALEMEL33243OLIA'''] diff --git a/cli/testdata/config/allow_path.toml b/cli/testdata/config/allow_path.toml deleted file mode 100644 index 0fa837701..000000000 --- a/cli/testdata/config/allow_path.toml +++ /dev/null @@ -1,9 +0,0 @@ -title = "simple config with allowlist for .go files" - -[[rules]] - description = "AWS Access Key" - id = "aws-access-key" - regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' - tags = ["key", "AWS"] - [rules.allowlist] - paths = ['''.go'''] diff --git a/cli/testdata/config/bad_entropy_group.toml b/cli/testdata/config/bad_entropy_group.toml deleted file mode 100755 index 8e4d1c25c..000000000 --- a/cli/testdata/config/bad_entropy_group.toml +++ /dev/null @@ -1,8 +0,0 @@ -title = "gitleaks config" - -[[rules]] -id = "discord-api-key" -description = "Discord API key" -regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]''' -secretGroup = 5 -entropy = 3.5 diff --git a/cli/testdata/config/base.toml b/cli/testdata/config/base.toml deleted file mode 100644 index ba7b2ce2c..000000000 --- a/cli/testdata/config/base.toml +++ /dev/null @@ -1,10 +0,0 @@ -title = "gitleaks config" - -[extend] -path="../testdata/config/extend_1.toml" - -[[rules]] - description = "AWS Secret Key" - id = "aws-secret-key" - regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' - tags = ["key", "AWS"] diff --git a/cli/testdata/config/entropy_group.toml b/cli/testdata/config/entropy_group.toml deleted file mode 100755 index eacfc50ea..000000000 --- a/cli/testdata/config/entropy_group.toml +++ /dev/null @@ -1,8 +0,0 @@ -title = "gitleaks config" - -[[rules]] -id = "discord-api-key" -description = "Discord API key" -regex = '''(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]''' -secretGroup = 3 -entropy = 3.5 diff --git a/cli/testdata/config/escaped_character_group.toml b/cli/testdata/config/escaped_character_group.toml deleted file mode 100644 index b28039539..000000000 --- a/cli/testdata/config/escaped_character_group.toml +++ /dev/null @@ -1,8 +0,0 @@ -title = "gitleaks config" -# https://learnxinyminutes.com/docs/toml/ for toml reference - -[[rules]] - id = "pypi-upload-token" - description = "PyPI upload token" - regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}''' - tags = ["key", "pypi"] \ No newline at end of file diff --git a/cli/testdata/config/extend_1.toml b/cli/testdata/config/extend_1.toml deleted file mode 100644 index 1f4eec0f0..000000000 --- a/cli/testdata/config/extend_1.toml +++ /dev/null @@ -1,10 +0,0 @@ -title = "gitleaks extended 1" - -[extend] -path="../testdata/config/extend_2.toml" - -[[rules]] - description = "AWS Access Key" - id = "aws-access-key" - regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' - tags = ["key", "AWS"] diff --git a/cli/testdata/config/extend_2.toml b/cli/testdata/config/extend_2.toml deleted file mode 100644 index 7532c99e6..000000000 --- a/cli/testdata/config/extend_2.toml +++ /dev/null @@ -1,10 +0,0 @@ -title = "gitleaks extended 2" - -[extend] -path="../testdata/config/extend_3.toml" - -[[rules]] - description = "AWS Secret Key" - id = "aws-secret-key-again" - regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' - tags = ["key", "AWS"] diff --git a/cli/testdata/config/extend_3.toml b/cli/testdata/config/extend_3.toml deleted file mode 100644 index 47644c296..000000000 --- a/cli/testdata/config/extend_3.toml +++ /dev/null @@ -1,9 +0,0 @@ -title = "gitleaks extended 3" - -## This should not be loaded since we can only extend configs to a depth of 3 - -[[rules]] - description = "AWS Secret Key" - id = "aws-secret-key-again-again" - regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' - tags = ["key", "AWS"] diff --git a/cli/testdata/config/generic.toml b/cli/testdata/config/generic.toml deleted file mode 100644 index 625e44efc..000000000 --- a/cli/testdata/config/generic.toml +++ /dev/null @@ -1,8 +0,0 @@ -title = "gitleaks config" - -[[rules]] -description = "Generic API Key" -id = "generic-api-key" -regex = '''(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]''' -entropy = 3.7 -secretGroup = 4 diff --git a/cli/testdata/config/generic_with_py_path.toml b/cli/testdata/config/generic_with_py_path.toml deleted file mode 100644 index a528893e9..000000000 --- a/cli/testdata/config/generic_with_py_path.toml +++ /dev/null @@ -1,36 +0,0 @@ -title = "gitleaks config" - -[[rules]] -description = "Generic API Key" -id = "generic-api-key" -regex = '''(?i)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9a-zA-Z\-_=]{8,64})['\"]''' -path = '''.py''' -entropy = 3.7 -secretGroup = 4 - -[allowlist] -description = "global allow lists" -regexes = [ - '''219-09-9999''', - '''078-05-1120''', - '''(9[0-9]{2}|666)-\d{2}-\d{4}''', - '''process''', - '''getenv''', - '''\.env''', - '''env\(''', - '''env\.''', - '''setting''', - '''load''', - '''token''', - '''password''', - '''secret''', - '''api\_key''', - '''apikey''', - '''api\-key''', - ] -paths = [ - '''gitleaks.toml''', - '''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$''', - '''(go.mod|go.sum)$''' -] - diff --git a/cli/testdata/config/path_only.toml b/cli/testdata/config/path_only.toml deleted file mode 100644 index 97a8a4870..000000000 --- a/cli/testdata/config/path_only.toml +++ /dev/null @@ -1,6 +0,0 @@ -title = "gitleaks config" - -[[rules]] -description = "Python Files" -id = "python-files-only" -path = '''.py''' diff --git a/cli/testdata/config/simple.toml b/cli/testdata/config/simple.toml deleted file mode 100644 index c5fbea1c3..000000000 --- a/cli/testdata/config/simple.toml +++ /dev/null @@ -1,222 +0,0 @@ -title = "gitleaks config" -# https://learnxinyminutes.com/docs/toml/ for toml reference - -[[rules]] - description = "AWS Access Key" - id = "aws-access-key" - regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' - tags = ["key", "AWS"] - -[[rules]] - description = "AWS Secret Key" - id = "aws-secret-key" - regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}''' - tags = ["key", "AWS"] - -[[rules]] - description = "AWS MWS key" - id = "aws-mws-key" - regex = '''amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}''' - tags = ["key", "AWS", "MWS"] - -[[rules]] - description = "Facebook Secret Key" - id = "facebook-secret-key" - regex = '''(?i)(facebook|fb)(.{0,20})?(?-i)['\"][0-9a-f]{32}['\"]''' - tags = ["key", "Facebook"] - -[[rules]] - description = "Facebook Client ID" - id = "facebook-client-id" - regex = '''(?i)(facebook|fb)(.{0,20})?['\"][0-9]{13,17}['\"]''' - tags = ["key", "Facebook"] - -[[rules]] - description = "Twitter Secret Key" - id = "twitter-secret-key" - regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{35,44}['\"]''' - tags = ["key", "Twitter"] - -[[rules]] - description = "Twitter Client ID" - id = "twitter-client-id" - regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{18,25}['\"]''' - tags = ["client", "Twitter"] - -[[rules]] - description = "Github Personal Access Token" - id = "github-pat" - regex = '''ghp_[0-9a-zA-Z]{36}''' - tags = ["key", "Github"] -[[rules]] - description = "Github OAuth Access Token" - id = "github-oauth" - regex = '''gho_[0-9a-zA-Z]{36}''' - tags = ["key", "Github"] -[[rules]] - id = "github-app" - description = "Github App Token" - regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}''' - tags = ["key", "Github"] -[[rules]] - id = "github-refresh" - description = "Github Refresh Token" - regex = '''ghr_[0-9a-zA-Z]{76}''' - tags = ["key", "Github"] - -[[rules]] - id = "linkedin-client" - description = "LinkedIn Client ID" - regex = '''(?i)linkedin(.{0,20})?(?-i)[0-9a-z]{12}''' - tags = ["client", "LinkedIn"] - -[[rules]] - id = "linkedin-secret" - description = "LinkedIn Secret Key" - regex = '''(?i)linkedin(.{0,20})?[0-9a-z]{16}''' - tags = ["secret", "LinkedIn"] - -[[rules]] - id = "slack" - description = "Slack" - regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?''' - tags = ["key", "Slack"] - -[[rules]] - id = "apkey" - description = "Asymmetric Private Key" - regex = '''-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----''' - tags = ["key", "AsymmetricPrivateKey"] - -[[rules]] - id = "google" - description = "Google API key" - regex = '''AIza[0-9A-Za-z\-_]{35}''' - tags = ["key", "Google"] - -[[rules]] - id = "google" - description = "Google (GCP) Service Account" - regex = '''"type": "service_account"''' - tags = ["key", "Google"] - -[[rules]] - id = "heroku" - description = "Heroku API key" - regex = '''(?i)heroku(.{0,20})?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}''' - tags = ["key", "Heroku"] - -[[rules]] - id = "mailchimp" - description = "MailChimp API key" - regex = '''(?i)(mailchimp|mc)(.{0,20})?[0-9a-f]{32}-us[0-9]{1,2}''' - tags = ["key", "Mailchimp"] - -[[rules]] - id = "mailgun" - description = "Mailgun API key" - regex = '''((?i)(mailgun|mg)(.{0,20})?)?key-[0-9a-z]{32}''' - tags = ["key", "Mailgun"] - -[[rules]] - id = "paypal" - description = "PayPal Braintree access token" - regex = '''access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}''' - tags = ["key", "Paypal"] - -[[rules]] - id = "piacatic" - description = "Picatic API key" - regex = '''sk_live_[0-9a-z]{32}''' - tags = ["key", "Picatic"] - -[[rules]] - id = "sendgrid" - description = "SendGrid API Key" - regex = '''SG\.[\w_]{16,32}\.[\w_]{16,64}''' - tags = ["key", "SendGrid"] - -[[rules]] - description = "Sidekiq Secret" - id = "sidekiq-secret" - regex = '''(?i)(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:['|\"|\n|\r|\s|\x60|;]|$)''' - secretGroup = 1 - keywords = [ - "bundle_enterprise__contribsys__com","bundle_gems__contribsys__com", - ] - -[[rules]] - description = "Sidekiq Sensitive URL" - id = "sidekiq-sensitive-url" - regex = '''(?i)\b(http(?:s??):\/\/)([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)''' - secretGroup = 2 - keywords = [ - "gems.contribsys.com","enterprise.contribsys.com", - ] - -[[rules]] - id = "slack-webhook" - description = "Slack Webhook" - regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}''' - tags = ["key", "slack"] - -[[rules]] - id = "stripe" - description = "Stripe API key" - regex = '''(?i)stripe(.{0,20})?[sr]k_live_[0-9a-zA-Z]{24}''' - tags = ["key", "Stripe"] - -[[rules]] - id = "square" - description = "Square access token" - regex = '''sq0atp-[0-9A-Za-z\-_]{22}''' - tags = ["key", "square"] - -[[rules]] - id = "square-oauth" - description = "Square OAuth secret" - regex = '''sq0csp-[0-9A-Za-z\-_]{43}''' - tags = ["key", "square"] - -[[rules]] - id = "twilio" - description = "Twilio API key" - regex = '''(?i)twilio(.{0,20})?SK[0-9a-f]{32}''' - tags = ["key", "twilio"] - -[[rules]] - id = "dynatrace" - description = "Dynatrace ttoken" - regex = '''dt0[a-zA-Z]{1}[0-9]{2}\.[A-Z0-9]{24}\.[A-Z0-9]{64}''' - tags = ["key", "Dynatrace"] - -[[rules]] - id = "shopify" - description = "Shopify shared secret" - regex = '''shpss_[a-fA-F0-9]{32}''' - tags = ["key", "Shopify"] - -[[rules]] - id = "shopify-access" - description = "Shopify access token" - regex = '''shpat_[a-fA-F0-9]{32}''' - tags = ["key", "Shopify"] - -[[rules]] - id = "shopify-custom" - description = "Shopify custom app access token" - regex = '''shpca_[a-fA-F0-9]{32}''' - tags = ["key", "Shopify"] - -[[rules]] - id = "shopify-private" - description = "Shopify private app access token" - regex = '''shppa_[a-fA-F0-9]{32}''' - tags = ["key", "Shopify"] - -[[rules]] - id = "pypi" - description = "PyPI upload token" - regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}''' - tags = ["key", "pypi"] - diff --git a/cli/testdata/expected/git/small-branch-foo.txt b/cli/testdata/expected/git/small-branch-foo.txt deleted file mode 100644 index b3554c7ac..000000000 --- a/cli/testdata/expected/git/small-branch-foo.txt +++ /dev/null @@ -1,17 +0,0 @@ -import ( - "fmt" - "os" -) - // seems safer - aws_token := os.Getenv("AWS_TOKEN") -package foo - -import "fmt" - -func Foo() { - fmt.Println("foo") - - // seems safe - aws_token := "AKIALALEMEL33243OLIA" - fmt.Println(aws_token) -} diff --git a/cli/testdata/expected/git/small.txt b/cli/testdata/expected/git/small.txt deleted file mode 100644 index 7235dd3a8..000000000 --- a/cli/testdata/expected/git/small.txt +++ /dev/null @@ -1,67 +0,0 @@ -import ( - "fmt" - "os" -) - // seems safer - aws_token := os.Getenv("AWS_TOKEN") -package foo - -import "fmt" - -func Foo() { - fmt.Println("foo") - - // seems safe - aws_token := "AKIALALEMEL33243OLIA" - fmt.Println(aws_token) -} -package api - -import "fmt" - -func PrintHello() { - fmt.Println("hello") -} -import ( - "fmt" - "os" -) - var a = "initial" - fmt.Println(a) - var b, c int = 1, 2 - fmt.Println(b, c) - var d = true - fmt.Println(d) - var e int - fmt.Println(e) - // load secret via env - awsToken := os.Getenv("AWS_TOKEN") - - f := "apple" - fmt.Println(f) - - // opps I added a secret at line 20 - awsToken := "AKIALALEMEL33243OLIA" -package main - -import "fmt" - -func main() { - - var a = "initial" - fmt.Println(a) - - var b, c int = 1, 2 - fmt.Println(b, c) - - var d = true - fmt.Println(d) - - var e int - fmt.Println(e) - - f := "apple" - fmt.Println(f) -} -# test -This is a repo used for testing gitleaks diff --git a/cli/testdata/expected/report/csv_simple.csv b/cli/testdata/expected/report/csv_simple.csv deleted file mode 100644 index a02ab0101..000000000 --- a/cli/testdata/expected/report/csv_simple.csv +++ /dev/null @@ -1,2 +0,0 @@ -RuleID,Commit,File,SymlinkFile,Secret,Match,StartLine,EndLine,StartColumn,EndColumn,Author,Message,Date,Email,Fingerprint -test-rule,0000000000000000,auth.py,,a secret,line containing secret,1,2,1,2,John Doe,opps,10-19-2003,johndoe@gmail.com,fingerprint diff --git a/cli/testdata/expected/report/empty.json b/cli/testdata/expected/report/empty.json deleted file mode 100644 index fe51488c7..000000000 --- a/cli/testdata/expected/report/empty.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/cli/testdata/expected/report/json_simple.json b/cli/testdata/expected/report/json_simple.json deleted file mode 100644 index c7516f118..000000000 --- a/cli/testdata/expected/report/json_simple.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "Description": "", - "StartLine": 1, - "EndLine": 2, - "StartColumn": 1, - "EndColumn": 2, - "Match": "line containing secret", - "Secret": "a secret", - "File": "auth.py", - "SymlinkFile": "", - "Commit": "0000000000000000", - "Entropy": 0, - "Author": "John Doe", - "Email": "johndoe@gmail.com", - "Date": "10-19-2003", - "Message": "opps", - "Tags": [], - "RuleID": "test-rule", - "Fingerprint": "" - } -] diff --git a/cli/testdata/expected/report/sarif_simple.got.sarif b/cli/testdata/expected/report/sarif_simple.got.sarif deleted file mode 100644 index 9708dd833..000000000 --- a/cli/testdata/expected/report/sarif_simple.got.sarif +++ /dev/null @@ -1,302 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/sarif-2.1.0.json", - "version": "2.1.0", - "runs": [ - { - "tool": { - "driver": { - "name": "gitleaks", - "semanticVersion": "v8.0.0", - "informationUri": "https://github.com/Infisical/infisical", - "rules": [ - { - "id": "aws-access-key", - "name": "AWS Access Key", - "shortDescription": { - "text": "(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}" - } - }, - { - "id": "aws-secret-key", - "name": "AWS Secret Key", - "shortDescription": { - "text": "(?i)aws_(.{0,20})?=?.[\\'\\\"0-9a-zA-Z\\/+]{40}" - } - }, - { - "id": "aws-mws-key", - "name": "AWS MWS key", - "shortDescription": { - "text": "amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" - } - }, - { - "id": "facebook-secret-key", - "name": "Facebook Secret Key", - "shortDescription": { - "text": "(?i)(facebook|fb)(.{0,20})?(?-i)['\\\"][0-9a-f]{32}['\\\"]" - } - }, - { - "id": "facebook-client-id", - "name": "Facebook Client ID", - "shortDescription": { - "text": "(?i)(facebook|fb)(.{0,20})?['\\\"][0-9]{13,17}['\\\"]" - } - }, - { - "id": "twitter-secret-key", - "name": "Twitter Secret Key", - "shortDescription": { - "text": "(?i)twitter(.{0,20})?['\\\"][0-9a-z]{35,44}['\\\"]" - } - }, - { - "id": "twitter-client-id", - "name": "Twitter Client ID", - "shortDescription": { - "text": "(?i)twitter(.{0,20})?['\\\"][0-9a-z]{18,25}['\\\"]" - } - }, - { - "id": "github-pat", - "name": "Github Personal Access Token", - "shortDescription": { - "text": "ghp_[0-9a-zA-Z]{36}" - } - }, - { - "id": "github-oauth", - "name": "Github OAuth Access Token", - "shortDescription": { - "text": "gho_[0-9a-zA-Z]{36}" - } - }, - { - "id": "github-app", - "name": "Github App Token", - "shortDescription": { - "text": "(ghu|ghs)_[0-9a-zA-Z]{36}" - } - }, - { - "id": "github-refresh", - "name": "Github Refresh Token", - "shortDescription": { - "text": "ghr_[0-9a-zA-Z]{76}" - } - }, - { - "id": "linkedin-client", - "name": "LinkedIn Client ID", - "shortDescription": { - "text": "(?i)linkedin(.{0,20})?(?-i)[0-9a-z]{12}" - } - }, - { - "id": "linkedin-secret", - "name": "LinkedIn Secret Key", - "shortDescription": { - "text": "(?i)linkedin(.{0,20})?[0-9a-z]{16}" - } - }, - { - "id": "slack", - "name": "Slack", - "shortDescription": { - "text": "xox[baprs]-([0-9a-zA-Z]{10,48})?" - } - }, - { - "id": "apkey", - "name": "Asymmetric Private Key", - "shortDescription": { - "text": "-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----" - } - }, - { - "id": "google", - "name": "Google (GCP) Service Account", - "shortDescription": { - "text": "\"type\": \"service_account\"" - } - }, - { - "id": "google", - "name": "Google (GCP) Service Account", - "shortDescription": { - "text": "\"type\": \"service_account\"" - } - }, - { - "id": "heroku", - "name": "Heroku API key", - "shortDescription": { - "text": "(?i)heroku(.{0,20})?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" - } - }, - { - "id": "mailchimp", - "name": "MailChimp API key", - "shortDescription": { - "text": "(?i)(mailchimp|mc)(.{0,20})?[0-9a-f]{32}-us[0-9]{1,2}" - } - }, - { - "id": "mailgun", - "name": "Mailgun API key", - "shortDescription": { - "text": "((?i)(mailgun|mg)(.{0,20})?)?key-[0-9a-z]{32}" - } - }, - { - "id": "paypal", - "name": "PayPal Braintree access token", - "shortDescription": { - "text": "access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}" - } - }, - { - "id": "piacatic", - "name": "Picatic API key", - "shortDescription": { - "text": "sk_live_[0-9a-z]{32}" - } - }, - { - "id": "sendgrid", - "name": "SendGrid API Key", - "shortDescription": { - "text": "SG\\.[\\w_]{16,32}\\.[\\w_]{16,64}" - } - }, - { - "id": "sidekiq-secret", - "name": "Sidekiq Secret", - "shortDescription": { - "text": "(?i)(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[0-9a-z\\-_\\t .]{0,20})(?:[\\s|']|[\\s|\"]){0,3}(?:=|\u003e|:=|\\|\\|:|\u003c=|=\u003e|:)(?:'|\\\"|\\s|=|\\x60){0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:['|\\\"|\\n|\\r|\\s|\\x60|;]|$)" - } - }, - { - "id": "sidekiq-sensitive-url", - "name": "Sidekiq Sensitive URL", - "shortDescription": { - "text": "(?i)\\b(http(?:s??):\\/\\/)([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\\/|\\#|\\?|:]|$)" - } - }, - { - "id": "slack-webhook", - "name": "Slack Webhook", - "shortDescription": { - "text": "https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}" - } - }, - { - "id": "stripe", - "name": "Stripe API key", - "shortDescription": { - "text": "(?i)stripe(.{0,20})?[sr]k_live_[0-9a-zA-Z]{24}" - } - }, - { - "id": "square", - "name": "Square access token", - "shortDescription": { - "text": "sq0atp-[0-9A-Za-z\\-_]{22}" - } - }, - { - "id": "square-oauth", - "name": "Square OAuth secret", - "shortDescription": { - "text": "sq0csp-[0-9A-Za-z\\-_]{43}" - } - }, - { - "id": "twilio", - "name": "Twilio API key", - "shortDescription": { - "text": "(?i)twilio(.{0,20})?SK[0-9a-f]{32}" - } - }, - { - "id": "dynatrace", - "name": "Dynatrace ttoken", - "shortDescription": { - "text": "dt0[a-zA-Z]{1}[0-9]{2}\\.[A-Z0-9]{24}\\.[A-Z0-9]{64}" - } - }, - { - "id": "shopify", - "name": "Shopify shared secret", - "shortDescription": { - "text": "shpss_[a-fA-F0-9]{32}" - } - }, - { - "id": "shopify-access", - "name": "Shopify access token", - "shortDescription": { - "text": "shpat_[a-fA-F0-9]{32}" - } - }, - { - "id": "shopify-custom", - "name": "Shopify custom app access token", - "shortDescription": { - "text": "shpca_[a-fA-F0-9]{32}" - } - }, - { - "id": "shopify-private", - "name": "Shopify private app access token", - "shortDescription": { - "text": "shppa_[a-fA-F0-9]{32}" - } - }, - { - "id": "pypi", - "name": "PyPI upload token", - "shortDescription": { - "text": "pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}" - } - } - ] - } - }, - "results": [ - { - "message": { - "text": "test-rule has detected secret for file auth.py at commit 0000000000000000." - }, - "ruleId": "test-rule", - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "auth.py" - }, - "region": { - "startLine": 1, - "startColumn": 1, - "endLine": 2, - "endColumn": 2, - "snippet": { - "text": "a secret" - } - } - } - } - ], - "partialFingerprints": { - "commitSha": "0000000000000000", - "email": "johndoe@gmail.com", - "author": "John Doe", - "date": "10-19-2003", - "commitMessage": "opps" - } - } - ] - } - ] -} diff --git a/cli/testdata/expected/report/sarif_simple.sarif b/cli/testdata/expected/report/sarif_simple.sarif deleted file mode 100644 index 0b1b15f70..000000000 --- a/cli/testdata/expected/report/sarif_simple.sarif +++ /dev/null @@ -1,302 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/sarif-2.1.0.json", - "version": "2.1.0", - "runs": [ - { - "tool": { - "driver": { - "name": "gitleaks", - "semanticVersion": "v8.0.0", - "informationUri": "https://github.com/gitleaks/gitleaks", - "rules": [ - { - "id": "aws-access-key", - "name": "AWS Access Key", - "shortDescription": { - "text": "(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}" - } - }, - { - "id": "aws-secret-key", - "name": "AWS Secret Key", - "shortDescription": { - "text": "(?i)aws_(.{0,20})?=?.[\\'\\\"0-9a-zA-Z\\/+]{40}" - } - }, - { - "id": "aws-mws-key", - "name": "AWS MWS key", - "shortDescription": { - "text": "amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" - } - }, - { - "id": "facebook-secret-key", - "name": "Facebook Secret Key", - "shortDescription": { - "text": "(?i)(facebook|fb)(.{0,20})?(?-i)['\\\"][0-9a-f]{32}['\\\"]" - } - }, - { - "id": "facebook-client-id", - "name": "Facebook Client ID", - "shortDescription": { - "text": "(?i)(facebook|fb)(.{0,20})?['\\\"][0-9]{13,17}['\\\"]" - } - }, - { - "id": "twitter-secret-key", - "name": "Twitter Secret Key", - "shortDescription": { - "text": "(?i)twitter(.{0,20})?['\\\"][0-9a-z]{35,44}['\\\"]" - } - }, - { - "id": "twitter-client-id", - "name": "Twitter Client ID", - "shortDescription": { - "text": "(?i)twitter(.{0,20})?['\\\"][0-9a-z]{18,25}['\\\"]" - } - }, - { - "id": "github-pat", - "name": "Github Personal Access Token", - "shortDescription": { - "text": "ghp_[0-9a-zA-Z]{36}" - } - }, - { - "id": "github-oauth", - "name": "Github OAuth Access Token", - "shortDescription": { - "text": "gho_[0-9a-zA-Z]{36}" - } - }, - { - "id": "github-app", - "name": "Github App Token", - "shortDescription": { - "text": "(ghu|ghs)_[0-9a-zA-Z]{36}" - } - }, - { - "id": "github-refresh", - "name": "Github Refresh Token", - "shortDescription": { - "text": "ghr_[0-9a-zA-Z]{76}" - } - }, - { - "id": "linkedin-client", - "name": "LinkedIn Client ID", - "shortDescription": { - "text": "(?i)linkedin(.{0,20})?(?-i)[0-9a-z]{12}" - } - }, - { - "id": "linkedin-secret", - "name": "LinkedIn Secret Key", - "shortDescription": { - "text": "(?i)linkedin(.{0,20})?[0-9a-z]{16}" - } - }, - { - "id": "slack", - "name": "Slack", - "shortDescription": { - "text": "xox[baprs]-([0-9a-zA-Z]{10,48})?" - } - }, - { - "id": "apkey", - "name": "Asymmetric Private Key", - "shortDescription": { - "text": "-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----" - } - }, - { - "id": "google", - "name": "Google (GCP) Service Account", - "shortDescription": { - "text": "\"type\": \"service_account\"" - } - }, - { - "id": "google", - "name": "Google (GCP) Service Account", - "shortDescription": { - "text": "\"type\": \"service_account\"" - } - }, - { - "id": "heroku", - "name": "Heroku API key", - "shortDescription": { - "text": "(?i)heroku(.{0,20})?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" - } - }, - { - "id": "mailchimp", - "name": "MailChimp API key", - "shortDescription": { - "text": "(?i)(mailchimp|mc)(.{0,20})?[0-9a-f]{32}-us[0-9]{1,2}" - } - }, - { - "id": "mailgun", - "name": "Mailgun API key", - "shortDescription": { - "text": "((?i)(mailgun|mg)(.{0,20})?)?key-[0-9a-z]{32}" - } - }, - { - "id": "paypal", - "name": "PayPal Braintree access token", - "shortDescription": { - "text": "access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}" - } - }, - { - "id": "piacatic", - "name": "Picatic API key", - "shortDescription": { - "text": "sk_live_[0-9a-z]{32}" - } - }, - { - "id": "sendgrid", - "name": "SendGrid API Key", - "shortDescription": { - "text": "SG\\.[\\w_]{16,32}\\.[\\w_]{16,64}" - } - }, - { - "id": "sidekiq-secret", - "name": "Sidekiq Secret", - "shortDescription": { - "text": "(?i)(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[0-9a-z\\-_\\t .]{0,20})(?:[\\s|']|[\\s|\"]){0,3}(?:=|\u003e|:=|\\|\\|:|\u003c=|=\u003e|:)(?:'|\\\"|\\s|=|\\x60){0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:['|\\\"|\\n|\\r|\\s|\\x60|;]|$)" - } - }, - { - "id": "sidekiq-sensitive-url", - "name": "Sidekiq Sensitive URL", - "shortDescription": { - "text": "(?i)\\b(http(?:s??):\\/\\/)([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\\/|\\#|\\?|:]|$)" - } - }, - { - "id": "slack-webhook", - "name": "Slack Webhook", - "shortDescription": { - "text": "https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}" - } - }, - { - "id": "stripe", - "name": "Stripe API key", - "shortDescription": { - "text": "(?i)stripe(.{0,20})?[sr]k_live_[0-9a-zA-Z]{24}" - } - }, - { - "id": "square", - "name": "Square access token", - "shortDescription": { - "text": "sq0atp-[0-9A-Za-z\\-_]{22}" - } - }, - { - "id": "square-oauth", - "name": "Square OAuth secret", - "shortDescription": { - "text": "sq0csp-[0-9A-Za-z\\-_]{43}" - } - }, - { - "id": "twilio", - "name": "Twilio API key", - "shortDescription": { - "text": "(?i)twilio(.{0,20})?SK[0-9a-f]{32}" - } - }, - { - "id": "dynatrace", - "name": "Dynatrace ttoken", - "shortDescription": { - "text": "dt0[a-zA-Z]{1}[0-9]{2}\\.[A-Z0-9]{24}\\.[A-Z0-9]{64}" - } - }, - { - "id": "shopify", - "name": "Shopify shared secret", - "shortDescription": { - "text": "shpss_[a-fA-F0-9]{32}" - } - }, - { - "id": "shopify-access", - "name": "Shopify access token", - "shortDescription": { - "text": "shpat_[a-fA-F0-9]{32}" - } - }, - { - "id": "shopify-custom", - "name": "Shopify custom app access token", - "shortDescription": { - "text": "shpca_[a-fA-F0-9]{32}" - } - }, - { - "id": "shopify-private", - "name": "Shopify private app access token", - "shortDescription": { - "text": "shppa_[a-fA-F0-9]{32}" - } - }, - { - "id": "pypi", - "name": "PyPI upload token", - "shortDescription": { - "text": "pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}" - } - } - ] - } - }, - "results": [ - { - "message": { - "text": "test-rule has detected secret for file auth.py at commit 0000000000000000." - }, - "ruleId": "test-rule", - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "auth.py" - }, - "region": { - "startLine": 1, - "startColumn": 1, - "endLine": 2, - "endColumn": 2, - "snippet": { - "text": "a secret" - } - } - } - } - ], - "partialFingerprints": { - "commitSha": "0000000000000000", - "email": "johndoe@gmail.com", - "author": "John Doe", - "date": "10-19-2003", - "commitMessage": "opps" - } - } - ] - } - ] -} diff --git a/cli/testdata/repos/nogit/main.go b/cli/testdata/repos/nogit/main.go deleted file mode 100644 index acbef43fd..000000000 --- a/cli/testdata/repos/nogit/main.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "fmt" - -func main() { - - var a = "initial" - fmt.Println(a) - - var b, c int = 1, 2 - fmt.Println(b, c) - - var d = true - fmt.Println(d) - - var e int - fmt.Println(e) - - // opps I added a secret at line 20 - awsToken := "AKIALALEMEL33243OLIA" - - f := "apple" - fmt.Println(f) -} diff --git a/cli/testdata/repos/small/README.md b/cli/testdata/repos/small/README.md deleted file mode 100644 index 5cc9baf4d..000000000 --- a/cli/testdata/repos/small/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# test -This is a repo used for testing gitleaks diff --git a/cli/testdata/repos/small/api/api.go b/cli/testdata/repos/small/api/api.go deleted file mode 100644 index d83247911..000000000 --- a/cli/testdata/repos/small/api/api.go +++ /dev/null @@ -1,7 +0,0 @@ -package api - -import "fmt" - -func PrintHello() { - fmt.Println("hello") -} diff --git a/cli/testdata/repos/small/dotGit/COMMIT_EDITMSG b/cli/testdata/repos/small/dotGit/COMMIT_EDITMSG deleted file mode 100644 index 0ba1543fd..000000000 --- a/cli/testdata/repos/small/dotGit/COMMIT_EDITMSG +++ /dev/null @@ -1 +0,0 @@ -removing secret from foo package diff --git a/cli/testdata/repos/small/dotGit/FETCH_HEAD b/cli/testdata/repos/small/dotGit/FETCH_HEAD deleted file mode 100644 index 66c1c77ce..000000000 --- a/cli/testdata/repos/small/dotGit/FETCH_HEAD +++ /dev/null @@ -1 +0,0 @@ -2e1db472eeba53f06c4026ae4566ea022e36598e branch 'main' of github.com:gitleaks/test diff --git a/cli/testdata/repos/small/dotGit/HEAD b/cli/testdata/repos/small/dotGit/HEAD deleted file mode 100644 index b870d8262..000000000 --- a/cli/testdata/repos/small/dotGit/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/main diff --git a/cli/testdata/repos/small/dotGit/ORIG_HEAD b/cli/testdata/repos/small/dotGit/ORIG_HEAD deleted file mode 100644 index 96321ccd4..000000000 --- a/cli/testdata/repos/small/dotGit/ORIG_HEAD +++ /dev/null @@ -1 +0,0 @@ -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 diff --git a/cli/testdata/repos/small/dotGit/config b/cli/testdata/repos/small/dotGit/config deleted file mode 100644 index 374df60b1..000000000 --- a/cli/testdata/repos/small/dotGit/config +++ /dev/null @@ -1,13 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - ignorecase = true - precomposeunicode = true -[remote "origin"] - url = git@github.com:gitleaks/test.git - fetch = +refs/heads/*:refs/remotes/origin/* -[branch "main"] - remote = origin - merge = refs/heads/main diff --git a/cli/testdata/repos/small/dotGit/description b/cli/testdata/repos/small/dotGit/description deleted file mode 100644 index 498b267a8..000000000 --- a/cli/testdata/repos/small/dotGit/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/cli/testdata/repos/small/dotGit/index b/cli/testdata/repos/small/dotGit/index deleted file mode 100644 index fec9889ae79cb0af45c8665b198e56dd8d7c80d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317 zcmZ?q402{*U|<4b<|NgEnTjtrMZ;)D1_pixmjjO(7#f!_Ffe`vsu2NVgP4=MzTD)0 z%W!UW=m}>Yjr(?v>022%gIpb5d|mZ&Qy9SdU!VR2qoL+JMKj0!hLQV3kvrMX73%KP zn4g!s%d?E-_u#tY>PUcwTIjb}ZyF9^;An#aP-#~Ola9;5WE z$$I9mR{l=eFwgzzi^!9kPMmgM%fOzSn3)GLEF{R)6=qk^szME`|t0UuMCV?jzSJQU!;9}HeGn>%QuBR M$B!>J{IKCX0RPQ#yZ`_I diff --git a/cli/testdata/repos/small/dotGit/info/exclude b/cli/testdata/repos/small/dotGit/info/exclude deleted file mode 100644 index a5196d1be..000000000 --- a/cli/testdata/repos/small/dotGit/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/cli/testdata/repos/small/dotGit/logs/HEAD b/cli/testdata/repos/small/dotGit/logs/HEAD deleted file mode 100644 index 8fc59bb3b..000000000 --- a/cli/testdata/repos/small/dotGit/logs/HEAD +++ /dev/null @@ -1,13 +0,0 @@ -0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896362 -0500 checkout: moving from main to remove-secrets -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 906335481df9a4b48906c90318b4fac76b67fe73 Zach Rice 1635896426 -0500 commit: load token via env var -906335481df9a4b48906c90318b4fac76b67fe73 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896518 -0500 commit: add api package -a122b33c6bad3ee54724f52f2caad385ab1982ab a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896543 -0500 checkout: moving from remove-secrets to api-pkg -a122b33c6bad3ee54724f52f2caad385ab1982ab 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896644 -0500 checkout: moving from api-pkg to main -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: Fast-forward -2e1db472eeba53f06c4026ae4566ea022e36598e 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896716 -0500 checkout: moving from main to foo -2e1db472eeba53f06c4026ae4566ea022e36598e 491504d5a31946ce75e22554cc34203d8e5ff3ca Zach Rice 1635896886 -0500 commit: adding foo package with secret -491504d5a31946ce75e22554cc34203d8e5ff3ca f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896931 -0500 commit: removing secret from foo package -f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635897009 -0500 checkout: moving from foo to main -2e1db472eeba53f06c4026ae4566ea022e36598e f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635897062 -0500 checkout: moving from main to foo -f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635897508 -0500 checkout: moving from foo to main diff --git a/cli/testdata/repos/small/dotGit/logs/refs/heads/api-pkg b/cli/testdata/repos/small/dotGit/logs/refs/heads/api-pkg deleted file mode 100644 index 18e1cff1a..000000000 --- a/cli/testdata/repos/small/dotGit/logs/refs/heads/api-pkg +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896543 -0500 branch: Created from HEAD diff --git a/cli/testdata/repos/small/dotGit/logs/refs/heads/foo b/cli/testdata/repos/small/dotGit/logs/refs/heads/foo deleted file mode 100644 index 0588ad530..000000000 --- a/cli/testdata/repos/small/dotGit/logs/refs/heads/foo +++ /dev/null @@ -1,3 +0,0 @@ -0000000000000000000000000000000000000000 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896716 -0500 branch: Created from HEAD -2e1db472eeba53f06c4026ae4566ea022e36598e 491504d5a31946ce75e22554cc34203d8e5ff3ca Zach Rice 1635896886 -0500 commit: adding foo package with secret -491504d5a31946ce75e22554cc34203d8e5ff3ca f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896931 -0500 commit: removing secret from foo package diff --git a/cli/testdata/repos/small/dotGit/logs/refs/heads/main b/cli/testdata/repos/small/dotGit/logs/refs/heads/main deleted file mode 100644 index 50148f0e8..000000000 --- a/cli/testdata/repos/small/dotGit/logs/refs/heads/main +++ /dev/null @@ -1,2 +0,0 @@ -0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: Fast-forward diff --git a/cli/testdata/repos/small/dotGit/logs/refs/heads/remove-secrets b/cli/testdata/repos/small/dotGit/logs/refs/heads/remove-secrets deleted file mode 100644 index 58344a340..000000000 --- a/cli/testdata/repos/small/dotGit/logs/refs/heads/remove-secrets +++ /dev/null @@ -1,3 +0,0 @@ -0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896362 -0500 branch: Created from HEAD -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 906335481df9a4b48906c90318b4fac76b67fe73 Zach Rice 1635896426 -0500 commit: load token via env var -906335481df9a4b48906c90318b4fac76b67fe73 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896518 -0500 commit: add api package diff --git a/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/HEAD b/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/HEAD deleted file mode 100644 index a2076e59a..000000000 --- a/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/HEAD +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git diff --git a/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/api-pkg b/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/api-pkg deleted file mode 100644 index 9c8e059cf..000000000 --- a/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/api-pkg +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896552 -0500 update by push diff --git a/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/foo b/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/foo deleted file mode 100644 index f6aed264f..000000000 --- a/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/foo +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896935 -0500 update by push diff --git a/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/main b/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/main deleted file mode 100644 index 530a7894d..000000000 --- a/cli/testdata/repos/small/dotGit/logs/refs/remotes/origin/main +++ /dev/null @@ -1 +0,0 @@ -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: fast-forward diff --git a/cli/testdata/repos/small/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 b/cli/testdata/repos/small/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 deleted file mode 100644 index dab89999a..000000000 --- a/cli/testdata/repos/small/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 +++ /dev/null @@ -1 +0,0 @@ -xUÌ1 Â0`×ܯ8nJ––.‚ƒƒ:*(8–´\¤´ɕ$ê þw3 Noxï{Ý$6Ëf1Û~´wF'0øYbF ŠœÏTB�p�ÐãND|ƒ*]uŽCÈSÐT •…ªkLÌ>a²Ž#(ûJm–‘®Ö(©ÚsæðԴ¹]Úëé°=õ÷ô>ð°ú03 \ No newline at end of file diff --git a/cli/testdata/repos/small/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 b/cli/testdata/repos/small/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 deleted file mode 100644 index f9ada07217b81e2055ac789cce7a4e74b3e98571..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmV-c0IC0Y0V^p=O;s>AVlXr?Ff%bx2y%6F@paY9O<{;Rx$DbK{Gumc8zsg>LAvJ2Q?U=6|e)0ejD zt)0tgetI*~(;L#%b=d>mgkO42DZqp&I%S5!poy_gGmUDhoM{pxB$LHVq=Znz(1aFo zdhLOPwG1gTHXw$ODik3}oNpMEs-{Fu31Q5LYUqFUNP-Z{GSM)~K=|mDM-W)Zc-m$*N&$nrMP2Q58jD zrn9S_a^RNz+knCIwc)g_rq{geivj;}e|3-se&PcNL%e zQ;;b4v@3VTLW-;)cgwUii_$DRy-9v ztpA;u;8UJc&<<4vIPC^{>%mtEFy7Q){)mdA%Wa9;@~OS#f?#DCm z+g3V|4{yDhfkYA1=qagX<|xr;!bzsd6A#D>1O;7fFcTTz3L-*%#1k5+r#H!0s2t=lIIJcc>)JTtswKy;@)7rXr{ dc9K%r?~rSSL-ZkcgP+n@c$zlr?hoKjPX=)8R5Snp diff --git a/cli/testdata/repos/small/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 b/cli/testdata/repos/small/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 deleted file mode 100644 index 5bddb82e2d848a5f668c70a9b3a5d8887c4cfc67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51 zcmV-30L=e*0V^p=O;s>9VK6i>Ff%bxNG!9VK6i>Ff%bxNXyUHOV4Lux)Bzh;A8T9`|r&v3yz)GXrp7E J0016I4}|e<6uAHZ diff --git a/cli/testdata/repos/small/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 b/cli/testdata/repos/small/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 deleted file mode 100644 index ce4a269d3..000000000 --- a/cli/testdata/repos/small/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 +++ /dev/null @@ -1,3 +0,0 @@ -x�ŽM -Â0F]çs%™üt -"žÁ¥»I2¡E۔»ðôö ®>x¼_ªË2w@;œz㑈ءCŒXЧ@‘²Ö6 5�)–ÔÆMÖ&†ÌÎF:l'ÌFÇTHďFFÃ1–äiPüéSmðä4ÁcN×o;¦ݷV{]žߗT—˜`=�Áa€³öZ«ƒ»ü«wå ½¾d…}f�u‡�›úKKœ \ No newline at end of file diff --git a/cli/testdata/repos/small/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad b/cli/testdata/repos/small/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad deleted file mode 100644 index 5e51e39d45702fbb232d53354c28fdca96718e6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 196 zcmV;#06YJ90bPzU3&JoEggrOQLKvy9UR0#P(i^_uI*Jq)1;)a4&r||DLB;W z;C}EY9EH@< zNm5?*h56Y^?UatmS4O8uu|%JFb(vx|wF3N!l{cJy^3FMD*&freTen98Evpv-5E*wztdudOGZmB9!Yu1$CX`zyIq5KJWzunN+wPUSfIx diff --git a/cli/testdata/repos/small/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab b/cli/testdata/repos/small/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab deleted file mode 100644 index fbcf357cc571a102e357b16d95a3538941f33de3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmV;U09^lg0iBLP4#FT106p`H{eT7vERY!E8$5bjmPH$FX$W3?y?ukHNhX( zV6yr!x(H;hE=Y~8PATUg1qHn=Xex8Dx@cjR7*TE1WgU<-TI*djr6zLO#a(dH*2L^8 zalnkBO0bGPXKbJNk9&ZM1cv`F-NHT)?39F`+jRj@oOjcDpYQ`72gb R3eYygM*O0Os2|U*Nw*#3PL%)v diff --git a/cli/testdata/repos/small/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 b/cli/testdata/repos/small/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 deleted file mode 100644 index 8be258a32c78adf07fdfe065081c2ef755049b8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmV;10D1p-0ac8#3c@fH0A2IG;_;?tv1nWr95PfWjp!dpS`sO3@=BB9ApX0Z1lQy4 z+L2q_spDp{-C&;%Ju^TbCZ02r519b`5<9#w7ZTqfR<^y*eavGlXthZKh_b}wTu7L# ocOdL5Ju(k2;^~Z}n_3%fs%vGG+8@--)SCWBzd*tVz6gvg!CC@8Z~y=R diff --git a/cli/testdata/repos/small/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a b/cli/testdata/repos/small/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a deleted file mode 100644 index 9221b3c0ad1fcb47f3445b805507bb6d16a28197..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143 zcmV;A0C4|!0V^p=O;s>7HexU|FfcPQQ3!H%bn$i7%S~a3Il1f0P5!qG=T?WFaOTmt zZ|9i4)x-b@6cP(E8Dc`}oJ4O%`dFIo{rC6FR|du`M40V^p=O;s>7Fk&z?FfcPQQ3!H%bn$i7%S~a3Il1f0P5!qG=T?WFaOTmt zZ|9i4)x-b@6cP(E8Dc`}oJ4O%`dFIo{rC6FR|du`M9WGF~X&Q45ERY)wz#Y>~ diff --git a/cli/testdata/repos/small/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb b/cli/testdata/repos/small/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb deleted file mode 100644 index 53b83ef007a757e589e3cf16407ff49717397595..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143 zcmV;A0C4|!0V^p=O;s>7HexU|FfcPQQ3!H%bn$i7%S~a3Il1f0P5!qG=T?WFaOTmt zZ|9i4)x-b@6cP(E8Dc`}oJ4O%`dFIo{rC6FR|du`M^uWjW>vuaQEcJLJ z>L?w@o3MG_#P_-@tE`tBRQ_N~E`9EzdVKecqb%|7rS!P8eom0mOD@ltuB>-{mwfbU z*H`zpt>JN+JJIsc*E~ykv8($JK7QPL1QK*?0YJa=0E;stV3xZI#0!8$LMSlF&IaPw vI(Ib7cDQF;Xqr=crnmpd4bOc^XAerFx|G@YAb-QBa2mkt=K#Lt>myWQtA&0RcKbuip?!xQV?Zf zfK&@@521UF$9mfA_7^N3u zXRrTixpZ@3B(aO$zo#ccuc`2{8TEYKk1*gD_t0slafXGv#MH>j|!Qu&8<5X z=&9g@H9H5gM!4BzT|YqQL9(m^AIL6Ewnbinu{6W&8mTvgnJ;&2){vv)4R0Ktd{6!m zyDzcXEU)dgFL{#r_X&kvIJH!Fiq`~LZ^paf^^HDnmK174*!i3s;hP_OSAdyhrH?}L zHD>oRn+z5=+w_kYWoO>Cj0T-naJCDpM==;$p>~NcyC~Zi1;i?1%Y`P-U`FCb*x*0c zVshQm#C1NNY0^Q8hBYH~L86JHo)sN1LW?b)nOxKEDUceaddeJ*#Cfb$FD@k;fe1o# zeLvPql}d0G!#rY0P+%`g&4dh{W^IH+8Ncd`C%lS8Ji960spKsoX(#WPG9el0^89(% zthmmuy%N67Am{}wGC*8#8Lc3Pk4)c9RrzY8emU{zvB8bN_~9i8ckId8qc4J&F_V0t z4p!|~L!k2lrhD2@DL?yu+s#IG=<+Pobnt%9!xb&@Fo0^5&I$fJQkl_^!BJM{UC*J> zDUnA$`P7*(sr3-^{<5fV+roJ=$t&om7$x=uqhW)K@;xAI+lP=XWnJhQ@w5)4lEcuD zpr9E{OnZGJbe_Do8&t}KzgNQs)EwlEkpRA}lXk^w|6L>?s?q|Gu;8;wSZd-lSM{#Q_-=Kra`A&KRB1jvazbngfBjgn&`bJzAm6xn_l`)i@ zC$=@Pd*bvFwpy;t+e;!<%5R0@d7Zet#GF;fo~7=Hlc=Ph&vp!o6n)1|pNN$sT1Z;X zQc>y){CpRFa{UDYhe6&L8n?TOhz~i=EC`q!6(=61|HK}hkLcE(*k?|CwVT-A;g?UT z=s;5;+S3p#&8wavOx4Tng??U|C4|AlUdY3kO=r?4C=n+@PC%$Ew@9;2KbGKGEf37Q z%H4V%PxSS3<@(|D-+bwO7sDM0El&*ZN?i{*W@p&OYmiyY#A|QIHR%OQaDc{f^!^*T zSmn@5sDrc5Gw248+-sVLn##n)){%^_fxOWRgBen?4BXHolf^npn?%alXh}-1f|&rf zFQS2?+5P}l&wYhEFeL~sduu^rs7p-SxbmUC2zp!ajlf8ihBY|A7ynBNa(LH8G(YB? z2wUnCqj}ltPGxu%sg-*%*n%1uYQn1%*iwB67_6`BE`*N7uw%!l&et=Hx>l&pM;#&y-Ve*8=RbEwEfn>P z0V~xEboE_zOkec&aqL!eh`%a!Efe4!4;}89QhiSI0$xkVv^Mf#OFg{R?6oh=3J>&a zc>^k~(<{H+OGu})zMM^OJydKA4h_Ux#)aCu+zh&{3j*X=18Hq%v85ryB++|dpulFg zE4w;hnQT52%z8It@nLc1ROuEt+wD0V1MVSIi#{Kz=(|JOS=aKqgV*(=+X{V~8oJw$ z--c(zfxWZ_#ffPahGJ;>Rr$8hfgVaX&hN2STiMyg?wovvkw%&!6fvCVvWhsK(=gl+ zC^y`Gh&_x}G|Y4-7CK_achoE#nuNJKSB-Y$aketH_^h1r-_?Yc-pi}`e`FB{slQ|X z73zy0a0$tyMlpT>t<8 diff --git a/cli/testdata/repos/small/dotGit/packed-refs b/cli/testdata/repos/small/dotGit/packed-refs deleted file mode 100644 index 859b8c5a8..000000000 --- a/cli/testdata/repos/small/dotGit/packed-refs +++ /dev/null @@ -1,2 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 refs/remotes/origin/main diff --git a/cli/testdata/repos/small/dotGit/refs/heads/api-pkg b/cli/testdata/repos/small/dotGit/refs/heads/api-pkg deleted file mode 100644 index 31b6c7893..000000000 --- a/cli/testdata/repos/small/dotGit/refs/heads/api-pkg +++ /dev/null @@ -1 +0,0 @@ -a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/cli/testdata/repos/small/dotGit/refs/heads/foo b/cli/testdata/repos/small/dotGit/refs/heads/foo deleted file mode 100644 index 57d584841..000000000 --- a/cli/testdata/repos/small/dotGit/refs/heads/foo +++ /dev/null @@ -1 +0,0 @@ -f1b58b97808f8e744f6a23c693859df5b5968901 diff --git a/cli/testdata/repos/small/dotGit/refs/heads/main b/cli/testdata/repos/small/dotGit/refs/heads/main deleted file mode 100644 index 98f12e928..000000000 --- a/cli/testdata/repos/small/dotGit/refs/heads/main +++ /dev/null @@ -1 +0,0 @@ -2e1db472eeba53f06c4026ae4566ea022e36598e diff --git a/cli/testdata/repos/small/dotGit/refs/heads/remove-secrets b/cli/testdata/repos/small/dotGit/refs/heads/remove-secrets deleted file mode 100644 index 31b6c7893..000000000 --- a/cli/testdata/repos/small/dotGit/refs/heads/remove-secrets +++ /dev/null @@ -1 +0,0 @@ -a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/cli/testdata/repos/small/dotGit/refs/remotes/origin/HEAD b/cli/testdata/repos/small/dotGit/refs/remotes/origin/HEAD deleted file mode 100644 index 4b0a87595..000000000 --- a/cli/testdata/repos/small/dotGit/refs/remotes/origin/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/remotes/origin/main diff --git a/cli/testdata/repos/small/dotGit/refs/remotes/origin/api-pkg b/cli/testdata/repos/small/dotGit/refs/remotes/origin/api-pkg deleted file mode 100644 index 31b6c7893..000000000 --- a/cli/testdata/repos/small/dotGit/refs/remotes/origin/api-pkg +++ /dev/null @@ -1 +0,0 @@ -a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/cli/testdata/repos/small/dotGit/refs/remotes/origin/foo b/cli/testdata/repos/small/dotGit/refs/remotes/origin/foo deleted file mode 100644 index 57d584841..000000000 --- a/cli/testdata/repos/small/dotGit/refs/remotes/origin/foo +++ /dev/null @@ -1 +0,0 @@ -f1b58b97808f8e744f6a23c693859df5b5968901 diff --git a/cli/testdata/repos/small/dotGit/refs/remotes/origin/main b/cli/testdata/repos/small/dotGit/refs/remotes/origin/main deleted file mode 100644 index 98f12e928..000000000 --- a/cli/testdata/repos/small/dotGit/refs/remotes/origin/main +++ /dev/null @@ -1 +0,0 @@ -2e1db472eeba53f06c4026ae4566ea022e36598e diff --git a/cli/testdata/repos/small/main.go b/cli/testdata/repos/small/main.go deleted file mode 100644 index 9a932e37e..000000000 --- a/cli/testdata/repos/small/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - "os" -) - -func main() { - - var a = "initial" - fmt.Println(a) - - var b, c int = 1, 2 - fmt.Println(b, c) - - var d = true - fmt.Println(d) - - var e int - fmt.Println(e) - - // load secret via env - awsToken := os.Getenv("AWS_TOKEN") - - f := "apple" - fmt.Println(f) -} diff --git a/cli/testdata/repos/staged/.gitleaksignore b/cli/testdata/repos/staged/.gitleaksignore deleted file mode 100644 index 770453ca4..000000000 --- a/cli/testdata/repos/staged/.gitleaksignore +++ /dev/null @@ -1 +0,0 @@ -api/api.go:aws-access-key:6 \ No newline at end of file diff --git a/cli/testdata/repos/staged/README.md b/cli/testdata/repos/staged/README.md deleted file mode 100644 index 5cc9baf4d..000000000 --- a/cli/testdata/repos/staged/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# test -This is a repo used for testing gitleaks diff --git a/cli/testdata/repos/staged/api/api.go b/cli/testdata/repos/staged/api/api.go deleted file mode 100644 index b16d768dd..000000000 --- a/cli/testdata/repos/staged/api/api.go +++ /dev/null @@ -1,10 +0,0 @@ -package api - -import "fmt" - -func PrintHello() { - aws_token := "AKIALALEMEL33243OLIA" // fingerprint of that secret is added to .gitleaksignore - aws_token2 := "AKIALALEMEL33243OLIA" // this one is not - fmt.Println(aws_token) - fmt.Println(aws_token2) -} diff --git a/cli/testdata/repos/staged/dotGit/COMMIT_EDITMSG b/cli/testdata/repos/staged/dotGit/COMMIT_EDITMSG deleted file mode 100644 index b83ad5393..000000000 --- a/cli/testdata/repos/staged/dotGit/COMMIT_EDITMSG +++ /dev/null @@ -1 +0,0 @@ -add .gitleaksignore file diff --git a/cli/testdata/repos/staged/dotGit/FETCH_HEAD b/cli/testdata/repos/staged/dotGit/FETCH_HEAD deleted file mode 100644 index 66c1c77ce..000000000 --- a/cli/testdata/repos/staged/dotGit/FETCH_HEAD +++ /dev/null @@ -1 +0,0 @@ -2e1db472eeba53f06c4026ae4566ea022e36598e branch 'main' of github.com:gitleaks/test diff --git a/cli/testdata/repos/staged/dotGit/HEAD b/cli/testdata/repos/staged/dotGit/HEAD deleted file mode 100644 index b870d8262..000000000 --- a/cli/testdata/repos/staged/dotGit/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/main diff --git a/cli/testdata/repos/staged/dotGit/ORIG_HEAD b/cli/testdata/repos/staged/dotGit/ORIG_HEAD deleted file mode 100644 index 96321ccd4..000000000 --- a/cli/testdata/repos/staged/dotGit/ORIG_HEAD +++ /dev/null @@ -1 +0,0 @@ -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 diff --git a/cli/testdata/repos/staged/dotGit/config b/cli/testdata/repos/staged/dotGit/config deleted file mode 100644 index 374df60b1..000000000 --- a/cli/testdata/repos/staged/dotGit/config +++ /dev/null @@ -1,13 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - ignorecase = true - precomposeunicode = true -[remote "origin"] - url = git@github.com:gitleaks/test.git - fetch = +refs/heads/*:refs/remotes/origin/* -[branch "main"] - remote = origin - merge = refs/heads/main diff --git a/cli/testdata/repos/staged/dotGit/description b/cli/testdata/repos/staged/dotGit/description deleted file mode 100644 index 498b267a8..000000000 --- a/cli/testdata/repos/staged/dotGit/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/cli/testdata/repos/staged/dotGit/index b/cli/testdata/repos/staged/dotGit/index deleted file mode 100644 index 42a3c4433f4d1e2ed0e9b4c42e0ca9a3c8fd5ecb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 359 zcmZ?q402{*U|<4bmK1T-AI9xbd@!1kfkB8-(e4EUL*o(#2F9;IH6lPP=O%IeNuYK% zx9sZH^DVjao!$LT>|@~9OV2FHNlnZy&P>nCFG>X}Oc7V&Q<-|u8B9-U*#I%m@EMwU z1~DgheYwg1mf_s$&=bx)8u#rS)3-8k2Dv)A_`2%lrU1>+n5M#XWgd)%nuG4H-y3tw zdaq7hI)6&lK8{L830uXvbs-E~i3OSZK&+RZ4|Eb3fZem~pE{Ucx>Olz-b++>F-p&x ztY`je 1635896329 -0500 clone: from github.com:gitleaks/test.git -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896362 -0500 checkout: moving from main to remove-secrets -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 906335481df9a4b48906c90318b4fac76b67fe73 Zach Rice 1635896426 -0500 commit: load token via env var -906335481df9a4b48906c90318b4fac76b67fe73 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896518 -0500 commit: add api package -a122b33c6bad3ee54724f52f2caad385ab1982ab a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896543 -0500 checkout: moving from remove-secrets to api-pkg -a122b33c6bad3ee54724f52f2caad385ab1982ab 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896644 -0500 checkout: moving from api-pkg to main -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: Fast-forward -2e1db472eeba53f06c4026ae4566ea022e36598e 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896716 -0500 checkout: moving from main to foo -2e1db472eeba53f06c4026ae4566ea022e36598e 491504d5a31946ce75e22554cc34203d8e5ff3ca Zach Rice 1635896886 -0500 commit: adding foo package with secret -491504d5a31946ce75e22554cc34203d8e5ff3ca f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896931 -0500 commit: removing secret from foo package -f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635897009 -0500 checkout: moving from foo to main -2e1db472eeba53f06c4026ae4566ea022e36598e f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635897062 -0500 checkout: moving from main to foo -f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635897508 -0500 checkout: moving from foo to main -2e1db472eeba53f06c4026ae4566ea022e36598e bf3f24164d7256b4021575cbdb2f97b98e6f057e Rafael Figueiredo 1679239434 -0300 commit: add .gitleaksignore file diff --git a/cli/testdata/repos/staged/dotGit/logs/refs/heads/api-pkg b/cli/testdata/repos/staged/dotGit/logs/refs/heads/api-pkg deleted file mode 100644 index 18e1cff1a..000000000 --- a/cli/testdata/repos/staged/dotGit/logs/refs/heads/api-pkg +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896543 -0500 branch: Created from HEAD diff --git a/cli/testdata/repos/staged/dotGit/logs/refs/heads/foo b/cli/testdata/repos/staged/dotGit/logs/refs/heads/foo deleted file mode 100644 index 0588ad530..000000000 --- a/cli/testdata/repos/staged/dotGit/logs/refs/heads/foo +++ /dev/null @@ -1,3 +0,0 @@ -0000000000000000000000000000000000000000 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896716 -0500 branch: Created from HEAD -2e1db472eeba53f06c4026ae4566ea022e36598e 491504d5a31946ce75e22554cc34203d8e5ff3ca Zach Rice 1635896886 -0500 commit: adding foo package with secret -491504d5a31946ce75e22554cc34203d8e5ff3ca f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896931 -0500 commit: removing secret from foo package diff --git a/cli/testdata/repos/staged/dotGit/logs/refs/heads/main b/cli/testdata/repos/staged/dotGit/logs/refs/heads/main deleted file mode 100644 index c4bd6cb68..000000000 --- a/cli/testdata/repos/staged/dotGit/logs/refs/heads/main +++ /dev/null @@ -1,3 +0,0 @@ -0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: Fast-forward -2e1db472eeba53f06c4026ae4566ea022e36598e bf3f24164d7256b4021575cbdb2f97b98e6f057e Rafael Figueiredo 1679239434 -0300 commit: add .gitleaksignore file diff --git a/cli/testdata/repos/staged/dotGit/logs/refs/heads/remove-secrets b/cli/testdata/repos/staged/dotGit/logs/refs/heads/remove-secrets deleted file mode 100644 index 58344a340..000000000 --- a/cli/testdata/repos/staged/dotGit/logs/refs/heads/remove-secrets +++ /dev/null @@ -1,3 +0,0 @@ -0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896362 -0500 branch: Created from HEAD -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 906335481df9a4b48906c90318b4fac76b67fe73 Zach Rice 1635896426 -0500 commit: load token via env var -906335481df9a4b48906c90318b4fac76b67fe73 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896518 -0500 commit: add api package diff --git a/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/HEAD b/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/HEAD deleted file mode 100644 index a2076e59a..000000000 --- a/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/HEAD +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice 1635896329 -0500 clone: from github.com:gitleaks/test.git diff --git a/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/api-pkg b/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/api-pkg deleted file mode 100644 index 9c8e059cf..000000000 --- a/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/api-pkg +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 a122b33c6bad3ee54724f52f2caad385ab1982ab Zach Rice 1635896552 -0500 update by push diff --git a/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/foo b/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/foo deleted file mode 100644 index f6aed264f..000000000 --- a/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/foo +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice 1635896935 -0500 update by push diff --git a/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/main b/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/main deleted file mode 100644 index 530a7894d..000000000 --- a/cli/testdata/repos/staged/dotGit/logs/refs/remotes/origin/main +++ /dev/null @@ -1 +0,0 @@ -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice 1635896648 -0500 pull origin main: fast-forward diff --git a/cli/testdata/repos/staged/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 b/cli/testdata/repos/staged/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 deleted file mode 100644 index dab89999a..000000000 --- a/cli/testdata/repos/staged/dotGit/objects/02/d85657604c34e7b7fbb324a0c6c8b13c2c3760 +++ /dev/null @@ -1 +0,0 @@ -xUÌ1 Â0`×ܯ8nJ––.‚ƒƒ:*(8–´\¤´ɕ$ê þw3 Noxï{Ý$6Ëf1Û~´wF'0øYbF ŠœÏTB�p�ÐãND|ƒ*]uŽCÈSÐT •…ªkLÌ>a²Ž#(ûJm–‘®Ö(©ÚsæðԴ¹]Úëé°=õ÷ô>ð°ú03 \ No newline at end of file diff --git a/cli/testdata/repos/staged/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 b/cli/testdata/repos/staged/dotGit/objects/15/2888a42422b2ff5868b8d003d626120a9cb738 deleted file mode 100644 index f9ada07217b81e2055ac789cce7a4e74b3e98571..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmV-c0IC0Y0V^p=O;s>AVlXr?Ff%bx2y%6F@paY9O<{;Rx$DbK{Gumc8zsg>LAvJ2Q?U=6|e)0ejD zt)0tgetI*~(;L#%b=d>mgkO42DZqp&I%S5!poy_gGmUDhoM{pxB$LHVq=Znz(1aFo zdhLOPwG1gTHXw$ODik3}oNpMEs-{Fu31Q5LYUqFUNP-Z{GSM)~K=|mDM-W)Zc-m$*N&$nrMP2Q58jD zrn9S_a^RNz+knCIwc)g_rq{geivj;}e|3-se&PcNL%e zQ;;b4v@3VTLW-;)cgwUii_$DRy-9v ztpA;u;8UJc&<<4vIPC^{>%mtEFy7Q){)mdA%Wa9;@~OS#f?#DCm8U`Q;;)CXd{^n9zt@?zb@ z+g3V|4{yDhfkYA1=qagX<|xr;!bzsd6A#D>1O;7fFcTTz3L-*%#1k5+r#H!0s2t=lIIJcc>)JTtswKy;@)7rXr{ dc9K%r?~rSSL-ZkcgP+n@c$zlr?hoKjPX=)8R5Snp diff --git a/cli/testdata/repos/staged/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 b/cli/testdata/repos/staged/dotGit/objects/5c/547e4215d9594c3935bdfefdf4f500016a4112 deleted file mode 100644 index 5bddb82e2d848a5f668c70a9b3a5d8887c4cfc67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51 zcmV-30L=e*0V^p=O;s>9VK6i>Ff%bxNG!40ZYosPf{>6G+-!5OwLYBPgO`P$mHV6%q_?-Dp62M%Pmpj;z}#cOI8Rd z%FHYANX^N~*U(g`=Hg5&FODzC&rZ!#u(DN9a`g6e^l|iY^>y_zHa0Rb_V@90RN~?U W>emBn%*oS$YtZDX7F=a3`FfcPQQP4}zEXhet%r4GM&&w}LWpI-m=4`OfZsC-y;=2Dv)A_`2%lrZB{u-1X%q|67K0t3yvX^Jv_+b4=fAVgLjRi3OPq zF(GwMqBkRbEKT?R`}^f917nt>5Y&j=#LPUs^n8X{ll9DBt^A#`VV?Wb7m+77ojC2j J767+rJXdN*O&|aO diff --git a/cli/testdata/repos/staged/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a b/cli/testdata/repos/staged/dotGit/objects/78/9ba677976d5db481de55c799d67acbf8e3f16a deleted file mode 100644 index 93acde0f58e563f655dbebe3a98dbf7b2cd06937..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51 zcmV-30L=e*0V^p=O;s>9VK6i>Ff%bxNXyUHOV4Lux)Bzh;A8T9`|r&v3yz)GXrp7E J0016I4}|e<6uAHZ diff --git a/cli/testdata/repos/staged/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 b/cli/testdata/repos/staged/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 deleted file mode 100644 index ce4a269d3..000000000 --- a/cli/testdata/repos/staged/dotGit/objects/90/6335481df9a4b48906c90318b4fac76b67fe73 +++ /dev/null @@ -1,3 +0,0 @@ -x�ŽM -Â0F]çs%™üt -"žÁ¥»I2¡E۔»ðôö ®>x¼_ªË2w@;œz㑈ءCŒXЧ@‘²Ö6 5�)–ÔÆMÖ&†ÌÎF:l'ÌFÇTHďFFÃ1–äiPüéSmðä4ÁcN×o;¦ݷV{]žߗT—˜`=�Áa€³öZ«ƒ»ü«wå ½¾d…}f�u‡�›úKKœ \ No newline at end of file diff --git a/cli/testdata/repos/staged/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad b/cli/testdata/repos/staged/dotGit/objects/9a/932e37eaa9fb64b09e47e5e859c9b2c8cb47ad deleted file mode 100644 index 5e51e39d45702fbb232d53354c28fdca96718e6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 196 zcmV;#06YJ90bPzU3&JoEggrOQLKvy9UR0#P(i^_uI*Jq)1;)a4&r||DLB;W z;C}EY9EH@< zNm5?*h56Y^?UatmS4O8uu|%JFb(vx|wF3N!l{cJy^3FMD*&freTen98Evpv-5E*wztdudOGZmB9!Yu1$CX`zyIq5KJWzunN+wPUSfIx diff --git a/cli/testdata/repos/staged/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab b/cli/testdata/repos/staged/dotGit/objects/a1/22b33c6bad3ee54724f52f2caad385ab1982ab deleted file mode 100644 index fbcf357cc571a102e357b16d95a3538941f33de3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmV;U09^lg0iBLP4#FT106p`H{eT7vERY!E8$5bjmPH$FX$W3?y?ukHNhX( zV6yr!x(H;hE=Y~8PATUg1qHn=Xex8Dx@cjR7*TE1WgU<-TI*djr6zLO#a(dH*2L^8 zalnkBO0bGPXKbJNk9&ZM1cv`F-NHT)?39F`+jRj@oOjcDpYQ`72gb R3eYygM*O0Os2|U*Nw*#3PL%)v diff --git a/cli/testdata/repos/staged/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 b/cli/testdata/repos/staged/dotGit/objects/a5/caae6d742e49a33982f1fdc608ce861ea59be5 deleted file mode 100644 index 8be258a32c78adf07fdfe065081c2ef755049b8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmV;10D1p-0ac8#3c@fH0A2IG;_;?tv1nWr95PfWjp!dpS`sO3@=BB9ApX0Z1lQy4 z+L2q_spDp{-C&;%Ju^TbCZ02r519b`5<9#w7ZTqfR<^y*eavGlXthZKh_b}wTu7L# ocOdL5Ju(k2;^~Z}n_3%fs%vGG+8@--)SCWBzd*tVz6gvg!CC@8Z~y=R diff --git a/cli/testdata/repos/staged/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a b/cli/testdata/repos/staged/dotGit/objects/a9/aa0c942dcef669a94f207a77426106b25efd1a deleted file mode 100644 index 9221b3c0ad1fcb47f3445b805507bb6d16a28197..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143 zcmV;A0C4|!0V^p=O;s>7HexU|FfcPQQ3!H%bn$i7%S~a3Il1f0P5!qG=T?WFaOTmt zZ|9i4)x-b@6cP(E8Dc`}oJ4O%`dFIo{rC6FR|du`MDU~-W38KgBEui|BvcRe1i2bg{QR>~g8%>k diff --git a/cli/testdata/repos/staged/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 b/cli/testdata/repos/staged/dotGit/objects/bc/f47ef84f29bb7ed6e653d61fccd30d0ecce886 deleted file mode 100644 index ec618b7a930fa1ad86255b0c385d1d9e439ff27d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116 zcmV-)0E_>40V^p=O;s>7Fk&z?FfcPQQ3!H%bn$i7%S~a3Il1f0P5!qG=T?WFaOTmt zZ|9i4)x-b@6cP(E8Dc`}oJ4O%`dFIo{rC6FR|du`M®­ӷÅF_uÝQ;r£S/ÿã²jozH[Ûó™&Y‚uÁ;OŸì˜Mú;;ðfÆhÎtXëؠ?ϺÞ[•ºÁü_]U \ No newline at end of file diff --git a/cli/testdata/repos/staged/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 b/cli/testdata/repos/staged/dotGit/objects/d8/32479114dc6be7207edc7c37ce91dd11b93161 deleted file mode 100644 index 0bd9a371a3ad63ecb59017199a81859c334ad129..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80 zcmV-W0I&ae0ZYosPf{>9WGF~X&Q45ERY)wz#Y>~ diff --git a/cli/testdata/repos/staged/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb b/cli/testdata/repos/staged/dotGit/objects/e5/c0849a65c586eab87dcfc31fec74f0fd7c62cb deleted file mode 100644 index 53b83ef007a757e589e3cf16407ff49717397595..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143 zcmV;A0C4|!0V^p=O;s>7HexU|FfcPQQ3!H%bn$i7%S~a3Il1f0P5!qG=T?WFaOTmt zZ|9i4)x-b@6cP(E8Dc`}oJ4O%`dFIo{rC6FR|du`M^uWjW>vuaQEcJLJ z>L?w@o3MG_#P_-@tE`tBRQ_N~E`9EzdVKecqb%|7rS!P8eom0mOD@ltuB>-{mwfbU z*H`zpt>JN+JJIsc*E~ykv8($JK7QPL1QK*?0YJa=0E;stV3xZI#0!8$LMSlF&IaPw vI(Ib7cDQF;Xqr=crnmpd4bOc^XAerFx|G@YAb-QBa2mkt=K#Lt>myWQtA&0RcKbuip?!xQV?Zf zfK&@@521UF$9mfA_7^N3u zXRrTixpZ@3B(aO$zo#ccuc`2{8TEYKk1*gD_t0slafXGv#MH>j|!Qu&8<5X z=&9g@H9H5gM!4BzT|YqQL9(m^AIL6Ewnbinu{6W&8mTvgnJ;&2){vv)4R0Ktd{6!m zyDzcXEU)dgFL{#r_X&kvIJH!Fiq`~LZ^paf^^HDnmK174*!i3s;hP_OSAdyhrH?}L zHD>oRn+z5=+w_kYWoO>Cj0T-naJCDpM==;$p>~NcyC~Zi1;i?1%Y`P-U`FCb*x*0c zVshQm#C1NNY0^Q8hBYH~L86JHo)sN1LW?b)nOxKEDUceaddeJ*#Cfb$FD@k;fe1o# zeLvPql}d0G!#rY0P+%`g&4dh{W^IH+8Ncd`C%lS8Ji960spKsoX(#WPG9el0^89(% zthmmuy%N67Am{}wGC*8#8Lc3Pk4)c9RrzY8emU{zvB8bN_~9i8ckId8qc4J&F_V0t z4p!|~L!k2lrhD2@DL?yu+s#IG=<+Pobnt%9!xb&@Fo0^5&I$fJQkl_^!BJM{UC*J> zDUnA$`P7*(sr3-^{<5fV+roJ=$t&om7$x=uqhW)K@;xAI+lP=XWnJhQ@w5)4lEcuD zpr9E{OnZGJbe_Do8&t}KzgNQs)EwlEkpRA}lXk^w|6L>?s?q|Gu;8;wSZd-lSM{#Q_-=Kra`A&KRB1jvazbngfBjgn&`bJzAm6xn_l`)i@ zC$=@Pd*bvFwpy;t+e;!<%5R0@d7Zet#GF;fo~7=Hlc=Ph&vp!o6n)1|pNN$sT1Z;X zQc>y){CpRFa{UDYhe6&L8n?TOhz~i=EC`q!6(=61|HK}hkLcE(*k?|CwVT-A;g?UT z=s;5;+S3p#&8wavOx4Tng??U|C4|AlUdY3kO=r?4C=n+@PC%$Ew@9;2KbGKGEf37Q z%H4V%PxSS3<@(|D-+bwO7sDM0El&*ZN?i{*W@p&OYmiyY#A|QIHR%OQaDc{f^!^*T zSmn@5sDrc5Gw248+-sVLn##n)){%^_fxOWRgBen?4BXHolf^npn?%alXh}-1f|&rf zFQS2?+5P}l&wYhEFeL~sduu^rs7p-SxbmUC2zp!ajlf8ihBY|A7ynBNa(LH8G(YB? z2wUnCqj}ltPGxu%sg-*%*n%1uYQn1%*iwB67_6`BE`*N7uw%!l&et=Hx>l&pM;#&y-Ve*8=RbEwEfn>P z0V~xEboE_zOkec&aqL!eh`%a!Efe4!4;}89QhiSI0$xkVv^Mf#OFg{R?6oh=3J>&a zc>^k~(<{H+OGu})zMM^OJydKA4h_Ux#)aCu+zh&{3j*X=18Hq%v85ryB++|dpulFg zE4w;hnQT52%z8It@nLc1ROuEt+wD0V1MVSIi#{Kz=(|JOS=aKqgV*(=+X{V~8oJw$ z--c(zfxWZ_#ffPahGJ;>Rr$8hfgVaX&hN2STiMyg?wovvkw%&!6fvCVvWhsK(=gl+ zC^y`Gh&_x}G|Y4-7CK_achoE#nuNJKSB-Y$aketH_^h1r-_?Yc-pi}`e`FB{slQ|X z73zy0a0$tyMlpT>t<8 diff --git a/cli/testdata/repos/staged/dotGit/packed-refs b/cli/testdata/repos/staged/dotGit/packed-refs deleted file mode 100644 index 859b8c5a8..000000000 --- a/cli/testdata/repos/staged/dotGit/packed-refs +++ /dev/null @@ -1,2 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 refs/remotes/origin/main diff --git a/cli/testdata/repos/staged/dotGit/refs/heads/api-pkg b/cli/testdata/repos/staged/dotGit/refs/heads/api-pkg deleted file mode 100644 index 31b6c7893..000000000 --- a/cli/testdata/repos/staged/dotGit/refs/heads/api-pkg +++ /dev/null @@ -1 +0,0 @@ -a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/cli/testdata/repos/staged/dotGit/refs/heads/foo b/cli/testdata/repos/staged/dotGit/refs/heads/foo deleted file mode 100644 index 57d584841..000000000 --- a/cli/testdata/repos/staged/dotGit/refs/heads/foo +++ /dev/null @@ -1 +0,0 @@ -f1b58b97808f8e744f6a23c693859df5b5968901 diff --git a/cli/testdata/repos/staged/dotGit/refs/heads/main b/cli/testdata/repos/staged/dotGit/refs/heads/main deleted file mode 100644 index a06d4d30f..000000000 --- a/cli/testdata/repos/staged/dotGit/refs/heads/main +++ /dev/null @@ -1 +0,0 @@ -bf3f24164d7256b4021575cbdb2f97b98e6f057e diff --git a/cli/testdata/repos/staged/dotGit/refs/heads/remove-secrets b/cli/testdata/repos/staged/dotGit/refs/heads/remove-secrets deleted file mode 100644 index 31b6c7893..000000000 --- a/cli/testdata/repos/staged/dotGit/refs/heads/remove-secrets +++ /dev/null @@ -1 +0,0 @@ -a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/cli/testdata/repos/staged/dotGit/refs/remotes/origin/HEAD b/cli/testdata/repos/staged/dotGit/refs/remotes/origin/HEAD deleted file mode 100644 index 4b0a87595..000000000 --- a/cli/testdata/repos/staged/dotGit/refs/remotes/origin/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/remotes/origin/main diff --git a/cli/testdata/repos/staged/dotGit/refs/remotes/origin/api-pkg b/cli/testdata/repos/staged/dotGit/refs/remotes/origin/api-pkg deleted file mode 100644 index 31b6c7893..000000000 --- a/cli/testdata/repos/staged/dotGit/refs/remotes/origin/api-pkg +++ /dev/null @@ -1 +0,0 @@ -a122b33c6bad3ee54724f52f2caad385ab1982ab diff --git a/cli/testdata/repos/staged/dotGit/refs/remotes/origin/foo b/cli/testdata/repos/staged/dotGit/refs/remotes/origin/foo deleted file mode 100644 index 57d584841..000000000 --- a/cli/testdata/repos/staged/dotGit/refs/remotes/origin/foo +++ /dev/null @@ -1 +0,0 @@ -f1b58b97808f8e744f6a23c693859df5b5968901 diff --git a/cli/testdata/repos/staged/dotGit/refs/remotes/origin/main b/cli/testdata/repos/staged/dotGit/refs/remotes/origin/main deleted file mode 100644 index 98f12e928..000000000 --- a/cli/testdata/repos/staged/dotGit/refs/remotes/origin/main +++ /dev/null @@ -1 +0,0 @@ -2e1db472eeba53f06c4026ae4566ea022e36598e diff --git a/cli/testdata/repos/staged/main.go b/cli/testdata/repos/staged/main.go deleted file mode 100644 index 9a932e37e..000000000 --- a/cli/testdata/repos/staged/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - "os" -) - -func main() { - - var a = "initial" - fmt.Println(a) - - var b, c int = 1, 2 - fmt.Println(b, c) - - var d = true - fmt.Println(d) - - var e int - fmt.Println(e) - - // load secret via env - awsToken := os.Getenv("AWS_TOKEN") - - f := "apple" - fmt.Println(f) -} diff --git a/cli/testdata/repos/symlinks/file_symlink/symlinked_id_ed25519 b/cli/testdata/repos/symlinks/file_symlink/symlinked_id_ed25519 deleted file mode 120000 index fd0203d88..000000000 --- a/cli/testdata/repos/symlinks/file_symlink/symlinked_id_ed25519 +++ /dev/null @@ -1 +0,0 @@ -../source_file/id_ed25519 \ No newline at end of file diff --git a/cli/testdata/repos/symlinks/source_file/id_ed25519 b/cli/testdata/repos/symlinks/source_file/id_ed25519 deleted file mode 100644 index 9f4ff614b..000000000 --- a/cli/testdata/repos/symlinks/source_file/id_ed25519 +++ /dev/null @@ -1,7 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACA8YWKYztuuvxUIMomc3zv0OdXCT57Cc2cRYu3TMbX9XAAAAJDiKO3C4ijt -wgAAAAtzc2gtZWQyNTUxOQAAACA8YWKYztuuvxUIMomc3zv0OdXCT57Cc2cRYu3TMbX9XA -AAAECzmj8DGxg5YHtBK4AmBttMXDQHsPAaCyYHQjJ4YujRBTxhYpjO266/FQgyiZzfO/Q5 -1cJPnsJzZxFi7dMxtf1cAAAADHJvb3RAZGV2aG9zdAE= ------END OPENSSH PRIVATE KEY----- diff --git a/cli/testdata/tmp/note.txt b/cli/testdata/tmp/note.txt deleted file mode 100644 index 429d60380..000000000 --- a/cli/testdata/tmp/note.txt +++ /dev/null @@ -1 +0,0 @@ -nothing should be saved here diff --git a/cli/upload_to_cloudsmith.sh b/cli/upload_to_cloudsmith.sh deleted file mode 100755 index 32a3694a4..000000000 --- a/cli/upload_to_cloudsmith.sh +++ /dev/null @@ -1,21 +0,0 @@ -cd dist -for i in *.apk; do - [ -f "$i" ] || break - cloudsmith push alpine --republish infisical/infisical-cli/alpine/any-version $i -done - -# for i in *.deb; do -# [ -f "$i" ] || break -# cloudsmith push deb --republish infisical/infisical-cli/any-distro/any-version $i -# done - -for i in *.deb; do - [ -f "$i" ] || break - deb-s3 upload --bucket=$INFISICAL_CLI_S3_BUCKET --prefix=deb --visibility=private --sign=$INFISICAL_CLI_REPO_SIGNING_KEY_ID --preserve-versions $i -done - - -for i in *.rpm; do - [ -f "$i" ] || break - cloudsmith push rpm --republish infisical/infisical-cli/any-distro/any-version $i -done From 9f0250caf265226f820109aed4a005f06090d5b4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 29 Jul 2025 20:54:55 +0800 Subject: [PATCH 74/79] misc: removed unnecessary CLI files in root --- .../workflows/release_build_infisical_cli.yml | 153 ----------- .github/workflows/run-cli-tests.yml | 55 ---- .goreleaser.yaml | 241 ------------------ npm/.eslintrc.json | 9 - npm/README.md | 68 ----- npm/package-lock.json | 141 ---------- npm/package.json | 25 -- npm/src/index.cjs | 166 ------------ 8 files changed, 858 deletions(-) delete mode 100644 .github/workflows/release_build_infisical_cli.yml delete mode 100644 .github/workflows/run-cli-tests.yml delete mode 100644 .goreleaser.yaml delete mode 100644 npm/.eslintrc.json delete mode 100644 npm/README.md delete mode 100644 npm/package-lock.json delete mode 100644 npm/package.json delete mode 100644 npm/src/index.cjs diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml deleted file mode 100644 index 1c16b00b3..000000000 --- a/.github/workflows/release_build_infisical_cli.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: Build and release CLI - -on: - workflow_dispatch: - - push: - # run only against tags - tags: - - "infisical-cli/v*.*.*" - -permissions: - contents: write - -jobs: - cli-integration-tests: - name: Run tests before deployment - uses: ./.github/workflows/run-cli-tests.yml - secrets: - CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }} - CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }} - CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }} - CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }} - CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} - CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }} - CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }} - CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} - - npm-release: - runs-on: ubuntu-latest - env: - working-directory: ./npm - needs: - - cli-integration-tests - - goreleaser - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Extract version - run: | - VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//') - echo "Version extracted: $VERSION" - echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV - - - name: Print version - run: echo ${{ env.CLI_VERSION }} - - - name: Setup Node - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 - with: - node-version: 20 - cache: "npm" - cache-dependency-path: ./npm/package-lock.json - - name: Install dependencies - working-directory: ${{ env.working-directory }} - run: npm install --ignore-scripts - - - name: Set NPM version - working-directory: ${{ env.working-directory }} - run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version - - - name: Setup NPM - working-directory: ${{ env.working-directory }} - run: | - echo 'registry="https://registry.npmjs.org/"' > ./.npmrc - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc - - echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Pack NPM - working-directory: ${{ env.working-directory }} - run: npm pack - - - name: Publish NPM - working-directory: ${{ env.working-directory }} - run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/ - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - goreleaser: - runs-on: ubuntu-latest-8-cores - needs: [cli-integration-tests] - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - run: git fetch --force --tags - - run: echo "Ref name ${{github.ref_name}}" - - uses: actions/setup-go@v3 - with: - go-version: ">=1.19.3" - cache: true - cache-dependency-path: cli/go.sum - - name: Setup for libssl1.0-dev - run: | - echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 - sudo apt update - sudo apt-get install -y libssl1.0-dev - - name: OSXCross for CGO Support - run: | - mkdir ../../osxcross - git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target - - uses: goreleaser/goreleaser-action@v4 - with: - distribution: goreleaser-pro - version: v1.26.2-pro - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} - POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} - FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} - AUR_KEY: ${{ secrets.AUR_KEY }} - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - - uses: actions/setup-python@v4 - - run: pip install --upgrade cloudsmith-cli - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 - with: - ruby-version: "3.3" # Not needed with a .ruby-version, .tool-versions or mise.toml - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - name: Install deb-s3 - run: gem install deb-s3 - - name: Configure GPG Key - run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --batch --import - env: - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} - GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }} - - name: Publish to CloudSmith - run: sh cli/upload_to_cloudsmith.sh - env: - CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} - INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }} - INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }} - AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} - - name: Invalidate Cloudfront cache - run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/deb/dists/stable/*' - env: - AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} - CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }} diff --git a/.github/workflows/run-cli-tests.yml b/.github/workflows/run-cli-tests.yml deleted file mode 100644 index da6f507a7..000000000 --- a/.github/workflows/run-cli-tests.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Go CLI Tests - -on: - pull_request: - types: [opened, synchronize] - paths: - - "cli/**" - - workflow_dispatch: - - workflow_call: - secrets: - CLI_TESTS_UA_CLIENT_ID: - required: true - CLI_TESTS_UA_CLIENT_SECRET: - required: true - CLI_TESTS_SERVICE_TOKEN: - required: true - CLI_TESTS_PROJECT_ID: - required: true - CLI_TESTS_ENV_SLUG: - required: true - CLI_TESTS_USER_EMAIL: - required: true - CLI_TESTS_USER_PASSWORD: - required: true - CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: - required: true -jobs: - test: - defaults: - run: - working-directory: ./cli - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version: "1.21.x" - - name: Install dependencies - run: go get . - - name: Test with the Go CLI - env: - CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }} - CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }} - CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }} - CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }} - CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }} - CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }} - CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }} - # INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} - - run: go test -v -count=1 ./test diff --git a/.goreleaser.yaml b/.goreleaser.yaml deleted file mode 100644 index e3147d650..000000000 --- a/.goreleaser.yaml +++ /dev/null @@ -1,241 +0,0 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com -# before: -# hooks: -# # You may remove this if you don't use go modules. -# - cd cli && go mod tidy -# # you may remove this if you don't need go generate -# - cd cli && go generate ./... -before: - hooks: - - ./cli/scripts/completions.sh - - ./cli/scripts/manpages.sh - -monorepo: - tag_prefix: infisical-cli/ - dir: cli - -builds: - - id: darwin-build - binary: infisical - ldflags: - - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} - flags: - - -trimpath - env: - - CGO_ENABLED=1 - - CC=/home/runner/work/osxcross/target/bin/o64-clang - - CXX=/home/runner/work/osxcross/target/bin/o64-clang++ - goos: - - darwin - ignore: - - goos: darwin - goarch: "386" - dir: ./cli - - - id: all-other-builds - env: - - CGO_ENABLED=0 - binary: infisical - ldflags: - - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} - flags: - - -trimpath - goos: - - freebsd - - linux - - netbsd - - openbsd - - windows - goarch: - - "386" - - amd64 - - arm - - arm64 - goarm: - - "6" - - "7" - ignore: - - goos: windows - goarch: "386" - - goos: freebsd - goarch: "386" - dir: ./cli - -archives: - - format_overrides: - - goos: windows - format: zip - files: - - ../README* - - ../LICENSE* - - ../manpages/* - - ../completions/* - -release: - replace_existing_draft: true - mode: "replace" - -checksum: - name_template: "checksums.txt" - -snapshot: - name_template: "{{ .Version }}-devel" - -# publishers: -# - name: fury.io -# ids: -# - infisical -# dir: "{{ dir .ArtifactPath }}" -# cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/ - -brews: - - name: infisical - tap: - owner: Infisical - name: homebrew-get-cli - commit_author: - name: "Infisical" - email: ai@infisical.com - folder: Formula - homepage: "https://infisical.com" - description: "The official Infisical CLI" - install: |- - bin.install "infisical" - bash_completion.install "completions/infisical.bash" => "infisical" - zsh_completion.install "completions/infisical.zsh" => "_infisical" - fish_completion.install "completions/infisical.fish" - man1.install "manpages/infisical.1.gz" - - name: "infisical@{{.Version}}" - tap: - owner: Infisical - name: homebrew-get-cli - commit_author: - name: "Infisical" - email: ai@infisical.com - folder: Formula - homepage: "https://infisical.com" - description: "The official Infisical CLI" - install: |- - bin.install "infisical" - bash_completion.install "completions/infisical.bash" => "infisical" - zsh_completion.install "completions/infisical.zsh" => "_infisical" - fish_completion.install "completions/infisical.fish" - man1.install "manpages/infisical.1.gz" - -nfpms: - - id: infisical - package_name: infisical - builds: - - all-other-builds - vendor: Infisical, Inc - homepage: https://infisical.com/ - maintainer: Infisical, Inc - description: The offical Infisical CLI - license: MIT - formats: - - rpm - - deb - - apk - - archlinux - bindir: /usr/bin - contents: - - src: ./completions/infisical.bash - dst: /etc/bash_completion.d/infisical - - src: ./completions/infisical.fish - dst: /usr/share/fish/vendor_completions.d/infisical.fish - - src: ./completions/infisical.zsh - dst: /usr/share/zsh/site-functions/_infisical - - src: ./manpages/infisical.1.gz - dst: /usr/share/man/man1/infisical.1.gz - -scoop: - bucket: - owner: Infisical - name: scoop-infisical - commit_author: - name: "Infisical" - email: ai@infisical.com - homepage: "https://infisical.com" - description: "The official Infisical CLI" - license: MIT - -winget: - - name: infisical - publisher: infisical - license: MIT - homepage: https://infisical.com - short_description: "The official Infisical CLI" - repository: - owner: infisical - name: winget-pkgs - branch: "infisical-{{.Version}}" - pull_request: - enabled: true - draft: false - base: - owner: microsoft - name: winget-pkgs - branch: master - -aurs: - - name: infisical-bin - homepage: "https://infisical.com" - description: "The official Infisical CLI" - maintainers: - - Infisical, Inc - license: MIT - private_key: "{{ .Env.AUR_KEY }}" - git_url: "ssh://aur@aur.archlinux.org/infisical-bin.git" - package: |- - # bin - install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical" - # license - install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE" - # completions - mkdir -p "${pkgdir}/usr/share/bash-completion/completions/" - mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" - mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" - install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical" - install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/_infisical" - install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish" - # man pages - install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz" - -dockers: - - dockerfile: docker/alpine - goos: linux - goarch: amd64 - use: buildx - ids: - - all-other-builds - image_templates: - - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64" - - "infisical/cli:latest-amd64" - build_flag_templates: - - "--pull" - - "--platform=linux/amd64" - - dockerfile: docker/alpine - goos: linux - goarch: amd64 - use: buildx - ids: - - all-other-builds - image_templates: - - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64" - - "infisical/cli:latest-arm64" - build_flag_templates: - - "--pull" - - "--platform=linux/arm64" - -docker_manifests: - - name_template: "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}" - image_templates: - - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64" - - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64" - - name_template: "infisical/cli:latest" - image_templates: - - "infisical/cli:latest-amd64" - - "infisical/cli:latest-arm64" diff --git a/npm/.eslintrc.json b/npm/.eslintrc.json deleted file mode 100644 index de8743bbb..000000000 --- a/npm/.eslintrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "env": { - "es6": true, - "node": true - }, - "parserOptions": { - "ecmaVersion": "latest" - } -} diff --git a/npm/README.md b/npm/README.md deleted file mode 100644 index febd52eec..000000000 --- a/npm/README.md +++ /dev/null @@ -1,68 +0,0 @@ -

Infisical CLI

-

-

Embrace shift-left security with the Infisical CLI and strengthen your DevSecOps practices by seamlessly managing secrets across your workflows, pipelines, and applications.

-

- -

- Slack | - Node.js SDK | - Infisical Cloud | - Self-Hosting | - Docs | - Website | - Hiring (Remote/SF) -

- -

- - Infisical is released under the MIT license. - - - PRs welcome! - - - git commit activity - - - Cloudsmith downloads - - - Slack community channel - - - Infisical Twitter - -

- -### Introduction - -The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables. You can use it across various environments, whether it’s local development, CI/CD, staging, or production. - -### Installation - -The Infisical CLI NPM package serves as a new installation method in addition to our [existing installation methods](https://infisical.com/docs/cli/overview). - -After installing the CLI with the command below, you'll be able to use the infisical CLI across your machine. - -```bash -$ npm install -g @infisical/cli -``` - -Full example: -```bash -# Install the Infisical CLI -$ npm install -g @infisical/cli - -# Authenticate with the Infisical CLI -$ infisical login - -# Initialize your Infisical CLI -$ infisical init - -# List your secrets with Infisical CLI -$ infisical secrets -``` - - -### Documentation -Our full CLI documentation can be found [here](https://infisical.com/docs/cli/usage). \ No newline at end of file diff --git a/npm/package-lock.json b/npm/package-lock.json deleted file mode 100644 index 0c3dea6ef..000000000 --- a/npm/package-lock.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "name": "@infisical/cli", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@infisical/cli", - "version": "0.0.0", - "hasInstallScript": true, - "dependencies": { - "tar": "^6.2.0", - "yauzl": "^3.2.0" - }, - "bin": { - "infisical": "bin/infisical" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yauzl": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", - "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "pend": "~1.2.0" - }, - "engines": { - "node": ">=12" - } - } - } -} diff --git a/npm/package.json b/npm/package.json deleted file mode 100644 index 0b9d0bbaf..000000000 --- a/npm/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@infisical/cli", - "private": false, - "version": "0.0.0", - "keywords": [ - "infisical", - "cli", - "command-line" - ], - "bin": { - "infisical": "./bin/infisical" - }, - "repository": { - "type": "git", - "url": "https://github.com/Infisical/infisical.git" - }, - "author": "Infisical Inc, ", - "scripts": { - "preinstall": "node src/index.cjs" - }, - "dependencies": { - "tar": "^6.2.0", - "yauzl": "^3.2.0" - } -} diff --git a/npm/src/index.cjs b/npm/src/index.cjs deleted file mode 100644 index f1ff51069..000000000 --- a/npm/src/index.cjs +++ /dev/null @@ -1,166 +0,0 @@ -const childProcess = require("child_process"); -const fs = require("fs"); -const stream = require("node:stream"); -const tar = require("tar"); -const path = require("path"); -const zlib = require("zlib"); -const yauzl = require("yauzl"); - -const packageJSON = require("../package.json"); - -const supportedPlatforms = ["linux", "darwin", "win32", "freebsd", "windows"]; -const outputDir = "bin"; - -const getPlatform = () => { - let platform = process.platform; - - if (platform === "win32") { - platform = "windows"; - } - - if (!supportedPlatforms.includes(platform)) { - console.error("Your platform doesn't seem to be of type darwin, linux or windows"); - process.exit(1); - } - return platform; -}; - -const getArchitecture = () => { - const architecture = process.arch; - let arch = ""; - - if (architecture === "x64" || architecture === "amd64") { - arch = "amd64"; - } else if (architecture === "arm64") { - arch = "arm64"; - } else if (architecture === "arm") { - // If the platform is Linux, we should find the exact ARM version, otherwise we default to armv7 which is the most common - if (process.platform === "linux" || process.platform === "freebsd") { - const output = childProcess.execSync("uname -m").toString().trim(); - - const armVersions = ["armv5", "armv6", "armv7"]; - - const armVersion = armVersions.find(version => output.startsWith(version)); - - if (armVersion) { - arch = armVersion; - } else { - arch = "armv7"; - } - } else { - arch = "armv7"; - } - } else if (architecture === "ia32") { - arch = "i386"; - } else { - console.error("Your architecture doesn't seem to be supported. Your architecture is", architecture); - process.exit(1); - } - - return arch; -}; - -async function extractZip(buffer, targetPath) { - return new Promise((resolve, reject) => { - yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => { - if (err) return reject(err); - - zipfile.readEntry(); - zipfile.on("entry", entry => { - const isExecutable = entry.fileName === "infisical" || entry.fileName === "infisical.exe"; - - if (/\/$/.test(entry.fileName) || !isExecutable) { - // Directory entry - zipfile.readEntry(); - } else { - // File entry - zipfile.openReadStream(entry, (err, readStream) => { - if (err) return reject(err); - - let fileName = entry.fileName; - - if (entry.fileName.endsWith(".exe")) { - fileName = "infisical.exe"; - } else if (entry.fileName.includes("infisical")) { - fileName = "infisical"; - } - - const outputPath = path.join(targetPath, fileName); - const writeStream = fs.createWriteStream(outputPath); - - readStream.pipe(writeStream); - writeStream.on("close", () => { - zipfile.readEntry(); - }); - }); - } - }); - - zipfile.on("end", resolve); - zipfile.on("error", reject); - }); - }); -} - -async function main() { - const PLATFORM = getPlatform(); - const ARCH = getArchitecture(); - const NUMERIC_RELEASE_VERSION = packageJSON.version; - const LATEST_RELEASE_VERSION = `v${NUMERIC_RELEASE_VERSION}`; - const EXTENSION = PLATFORM === "windows" ? "zip" : "tar.gz"; - const downloadLink = `https://github.com/Infisical/infisical/releases/download/infisical-cli/${LATEST_RELEASE_VERSION}/infisical_${NUMERIC_RELEASE_VERSION}_${PLATFORM}_${ARCH}.${EXTENSION}`; - - // Ensure the output directory exists - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir); - } - - // Download the latest CLI binary - try { - const response = await fetch(downloadLink, { - headers: { - Accept: "application/octet-stream" - } - }); - - if (!response.ok) { - throw new Error(`Failed to fetch: ${response.status} - ${response.statusText}`); - } - - if (EXTENSION === "zip") { - // For ZIP files, we need to buffer the whole thing first - const buffer = await response.arrayBuffer(); - await extractZip(Buffer.from(buffer), outputDir); - } else { - // For tar.gz files, we stream - await new Promise((resolve, reject) => { - const outStream = stream.Readable.fromWeb(response.body) - .pipe(zlib.createGunzip()) - .pipe( - tar.x({ - C: path.join(outputDir), - filter: path => path === "infisical" - }) - ); - - outStream.on("error", reject); - outStream.on("close", resolve); - }); - } - - // Platform-specific tasks - if (PLATFORM === "windows") { - // We create an empty file called 'infisical'. This file has no functionality, except allowing NPM to correctly create the symlink. - // Reason why this doesn't work without the empty file, is because the files downloaded are a .ps1, .exe, and .cmd file. None of these match the binary name from the package.json['bin'] field. - // This is a bit hacky, but it assures that the symlink is correctly created. - fs.closeSync(fs.openSync(path.join(outputDir, "infisical"), "w")); - } else { - // Unix systems only need chmod - fs.chmodSync(path.join(outputDir, "infisical"), "755"); - } - } catch (error) { - console.error("Error downloading or extracting Infisical CLI:", error); - process.exit(1); - } -} -main(); From 992df5c7d0717425afe6c6ed0a3611dfa61368d8 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 29 Jul 2025 14:22:39 -0300 Subject: [PATCH 75/79] Fix secret version history link to user/machine details page --- .../components/SecretListView/SecretDetailSidebar.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index fbd4565aa..92565b7ad 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -59,7 +59,6 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { useGetReminder } from "@app/hooks/api/reminders"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; -import { ProjectType } from "@app/hooks/api/workspace/types"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { camelCaseToSpaces } from "@app/lib/fn/string"; @@ -269,9 +268,9 @@ export const SecretDetailSidebar = ({ ) => { switch (actorType) { case ActorType.USER: - return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${membershipId}`; + return `/projects/secret-management/${currentWorkspace.id}/members/${membershipId}`; case ActorType.IDENTITY: - return `/${ProjectType.SecretManager}/${currentWorkspace.id}/identities/${actorId}`; + return `/projects/secret-management/${currentWorkspace.id}/identities/${actorId}`; default: return null; } From 561dbb88357e7ad3ab847d7d45ee0940712d8301 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 29 Jul 2025 23:57:46 +0400 Subject: [PATCH 76/79] fix(fips): x86 support --- .../release-standalone-docker-img-postgres-offical.yml | 7 +------ Dockerfile.fips.standalone-infisical | 7 +++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index 7a73288cb..812283b33 100644 --- a/.github/workflows/release-standalone-docker-img-postgres-offical.yml +++ b/.github/workflows/release-standalone-docker-img-postgres-offical.yml @@ -79,12 +79,7 @@ jobs: working-directory: backend - name: version output run: | - echo "Output Value: ${{ steps.version.outputs.major }}" - echo "Output Value: ${{ steps.version.outputs.minor }}" - echo "Output Value: ${{ steps.version.outputs.patch }}" - echo "Output Value: ${{ steps.version.outputs.version }}" - echo "Output Value: ${{ steps.version.outputs.version_type }}" - echo "Output Value: ${{ steps.version.outputs.increment }}" + echo "Version: ${{ steps.version.outputs.major }}" - name: Save commit hashes for tag id: commit uses: pr-mpt/actions-commit-hash@v2 diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index b95794832..974ce4a42 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -34,6 +34,8 @@ ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ARG CAPTCHA_SITE_KEY ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY +ENV NODE_OPTIONS="--max-old-space-size=8192" + # Build RUN npm run build @@ -209,6 +211,11 @@ EXPOSE 443 RUN grep -v 'import "./lib/telemetry/instrumentation.mjs";' dist/main.mjs > dist/main.mjs.tmp && \ mv dist/main.mjs.tmp dist/main.mjs +# The OpenSSL library is installed in different locations in different architectures (x86_64 and arm64). +# This is a workaround to avoid errors when the library is not found. +RUN ln -sf /usr/local/lib64/ossl-modules /usr/local/lib/ossl-modules || \ + ln -sf /usr/local/lib/ossl-modules /usr/local/lib64/ossl-modules + USER non-root-user CMD ["./standalone-entrypoint.sh"] \ No newline at end of file From 9374ee3c2efab6ea05c87e909f8f0a92e66cdd61 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 30 Jul 2025 03:57:59 +0800 Subject: [PATCH 77/79] doc: add bootstrap to API reference --- backend/src/server/routes/v1/admin-router.ts | 1 + .../api-reference/endpoints/admin/bootstrap-instance.mdx | 9 +++++++++ docs/docs.json | 6 +++++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 docs/api-reference/endpoints/admin/bootstrap-instance.mdx diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 57aa42fae..6ad368816 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -687,6 +687,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { + hide: false, body: z.object({ email: z.string().email().trim().min(1), password: z.string().trim().min(1), diff --git a/docs/api-reference/endpoints/admin/bootstrap-instance.mdx b/docs/api-reference/endpoints/admin/bootstrap-instance.mdx new file mode 100644 index 000000000..ce147bff1 --- /dev/null +++ b/docs/api-reference/endpoints/admin/bootstrap-instance.mdx @@ -0,0 +1,9 @@ +--- +title: "Bootstrap Instance" +openapi: "POST /api/v1/admin/bootstrap" +--- + + + For guidance, check out the docs for [Programmatic + Provisioning](/self-hosting/guides/automated-bootstrapping). + diff --git a/docs/docs.json b/docs/docs.json index ebbc31500..82988328c 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -717,6 +717,10 @@ { "group": "Endpoints", "pages": [ + { + "group": "Admin", + "pages": ["api-reference/endpoints/admin/bootstrap-instance"] + }, { "group": "Identities", "pages": [ @@ -1393,7 +1397,7 @@ "api-reference/endpoints/app-connections/databricks/delete" ] }, - { + { "group": "Digital Ocean", "pages": [ "api-reference/endpoints/app-connections/digital-ocean/list", From 40de8331a3305272b81896cd8e812dd207c13897 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 29 Jul 2025 16:58:06 -0300 Subject: [PATCH 78/79] Fix scim user updates setting isEmailVerified back to false when the email has not changed --- backend/src/ee/services/scim/scim-service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index ecb900e53..a87d67b94 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -579,6 +579,9 @@ export const scimServiceFactory = ({ }); const serverCfg = await getServerCfg(); + const hasEmailChanged = email?.toLowerCase() !== membership.email; + const defaultEmailVerified = + org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails; await userDAL.transaction(async (tx) => { await userAliasDAL.update( { @@ -605,8 +608,7 @@ export const scimServiceFactory = ({ firstName, email: email?.toLowerCase(), lastName, - isEmailVerified: - org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails + isEmailVerified: hasEmailChanged ? defaultEmailVerified : undefined }, tx ); From e2d48164655f6e94b958fc02cfa9294aae110048 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 30 Jul 2025 00:16:40 +0400 Subject: [PATCH 79/79] Update release-standalone-docker-img-postgres-offical.yml --- .../release-standalone-docker-img-postgres-offical.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-standalone-docker-img-postgres-offical.yml b/.github/workflows/release-standalone-docker-img-postgres-offical.yml index 812283b33..7a73288cb 100644 --- a/.github/workflows/release-standalone-docker-img-postgres-offical.yml +++ b/.github/workflows/release-standalone-docker-img-postgres-offical.yml @@ -79,7 +79,12 @@ jobs: working-directory: backend - name: version output run: | - echo "Version: ${{ steps.version.outputs.major }}" + echo "Output Value: ${{ steps.version.outputs.major }}" + echo "Output Value: ${{ steps.version.outputs.minor }}" + echo "Output Value: ${{ steps.version.outputs.patch }}" + echo "Output Value: ${{ steps.version.outputs.version }}" + echo "Output Value: ${{ steps.version.outputs.version_type }}" + echo "Output Value: ${{ steps.version.outputs.increment }}" - name: Save commit hashes for tag id: commit uses: pr-mpt/actions-commit-hash@v2