import * as Sentry from "@sentry/node"; import _ from 'lodash'; import AWS from 'aws-sdk'; import { SecretsManagerClient, UpdateSecretCommand, CreateSecretCommand, GetSecretValueCommand, ResourceNotFoundException } from '@aws-sdk/client-secrets-manager'; import { Octokit } from "@octokit/rest"; import sodium from "libsodium-wrappers"; import { IIntegration, IIntegrationAuth } from "../models"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, INTEGRATION_HEROKU_API_URL, INTEGRATION_GITLAB_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, INTEGRATION_TRAVISCI_API_URL, } from "../variables"; import request from '../config/request'; /** * Sync/push [secrets] to [app] in integration named [integration] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessId - access id for integration * @param {String} obj.accessToken - access token for integration */ const syncSecrets = async ({ integration, integrationAuth, secrets, accessId, accessToken, }: { integration: IIntegration; integrationAuth: IIntegrationAuth; secrets: any; accessId: string | null; accessToken: string; }) => { try { switch (integration.integration) { case INTEGRATION_AZURE_KEY_VAULT: await syncSecretsAzureKeyVault({ integration, secrets, accessToken }); break; case INTEGRATION_AWS_PARAMETER_STORE: await syncSecretsAWSParameterStore({ integration, secrets, accessId, accessToken }); break; case INTEGRATION_AWS_SECRET_MANAGER: await syncSecretsAWSSecretManager({ integration, secrets, accessId, accessToken }); break; case INTEGRATION_HEROKU: await syncSecretsHeroku({ integration, secrets, accessToken, }); break; case INTEGRATION_VERCEL: await syncSecretsVercel({ integration, integrationAuth, secrets, accessToken, }); break; case INTEGRATION_NETLIFY: await syncSecretsNetlify({ integration, integrationAuth, secrets, accessToken, }); break; case INTEGRATION_GITHUB: await syncSecretsGitHub({ integration, secrets, accessToken, }); break; case INTEGRATION_GITLAB: await syncSecretsGitLab({ integration, secrets, accessToken, }); break; case INTEGRATION_RENDER: await syncSecretsRender({ integration, secrets, accessToken, }); break; case INTEGRATION_FLYIO: await syncSecretsFlyio({ integration, secrets, accessToken, }); break; case INTEGRATION_CIRCLECI: await syncSecretsCircleCI({ integration, secrets, accessToken, }); break; case INTEGRATION_TRAVISCI: await syncSecretsTravisCI({ integration, secrets, accessToken, }); break; } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error('Failed to sync secrets to integration'); } }; /** * Sync/push [secrets] to Azure Key Vault with vault URI [integration.app] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for Azure Key Vault integration */ const syncSecretsAzureKeyVault = async ({ integration, secrets, accessToken }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { interface GetAzureKeyVaultSecret { id: string; // secret URI attributes: { enabled: true, created: number; updated: number; recoveryLevel: string; recoverableDays: number; } } interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { key: string; } /** * Return all secrets from Azure Key Vault by paginating through URL [url] * @param {String} url - pagination URL to get next set of secrets from Azure Key Vault * @returns */ const paginateAzureKeyVaultSecrets = async (url: string) => { let result: GetAzureKeyVaultSecret[] = []; try { while (url) { const res = await request.get(url, { headers: { Authorization: `Bearer ${accessToken}` } }); result = result.concat(res.data.value); url = res.data.nextLink; } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); } return result; } const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`); let lastSlashIndex: number; const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { if (!lastSlashIndex) { lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/'); } const azureKeyVaultSecret = await request.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); return ({ ...azureKeyVaultSecret.data, key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1), }); }))) .reduce((obj: any, secret: any) => ({ ...obj, [secret.key]: secret }), {}); const setSecrets: { key: string; value: string; }[] = []; Object.keys(secrets).forEach((key) => { const hyphenatedKey = key.replace(/_/g, '-'); if (!(hyphenatedKey in res)) { // case: secret has been created setSecrets.push({ key: hyphenatedKey, value: secrets[key] }); } else { if (secrets[key] !== res[hyphenatedKey].value) { // case: secret has been updated setSecrets.push({ key: hyphenatedKey, value: secrets[key] }); } } }); const deleteSecrets: AzureKeyVaultSecret[] = []; Object.keys(res).forEach((key) => { const underscoredKey = key.replace(/-/g, '_'); if (!(underscoredKey in secrets)) { deleteSecrets.push(res[key]); } }); const setSecretAzureKeyVault = async ({ key, value, integration, accessToken }: { key: string; value: string; integration: IIntegration; accessToken: string; }) => { let isSecretSet = false; let maxTries = 6; while (!isSecretSet && maxTries > 0) { // try to set secret try { await request.put( `${integration.app}/secrets/${key}?api-version=7.3`, { value }, { headers: { Authorization: `Bearer ${accessToken}` } } ); isSecretSet = true; } catch (err) { const error: any = err; if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') { await request.post( `${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {}, { headers: { Authorization: `Bearer ${accessToken}` } } ); await new Promise(resolve => setTimeout(resolve, 10000)); } else { await new Promise(resolve => setTimeout(resolve, 10000)); maxTries--; } } } } // Sync/push set secrets for await (const setSecret of setSecrets) { const { key, value } = setSecret; setSecretAzureKeyVault({ key, value, integration, accessToken }); } for await (const deleteSecret of deleteSecrets) { const { key } = deleteSecret; await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error('Failed to sync secrets to Azure Key Vault'); } }; /** * Sync/push [secrets] to AWS parameter store * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessId - access id for AWS parameter store integration * @param {String} obj.accessToken - access token for AWS parameter store integration */ const syncSecretsAWSParameterStore = async ({ integration, secrets, accessId, accessToken }: { integration: IIntegration; secrets: any; accessId: string | null; accessToken: string; }) => { try { if (!accessId) return; AWS.config.update({ region: integration.region, accessKeyId: accessId, secretAccessKey: accessToken }); const ssm = new AWS.SSM({ apiVersion: '2014-11-06', region: integration.region }); const params = { Path: integration.path, Recursive: true, WithDecryption: true }; const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters let awsParameterStoreSecretsObj: { [key: string]: any // TODO: fix type } = {}; if (parameterList) { awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({ ...obj, [secret.Name.split("/").pop()]: secret }), {}); } // Identify secrets to create Object.keys(secrets).map(async (key) => { if (!(key in awsParameterStoreSecretsObj)) { // case: secret does not exist in AWS parameter store // -> create secret await ssm.putParameter({ Name: `${integration.path}${key}`, Type: 'SecureString', Value: secrets[key], Overwrite: true }).promise(); } else { // case: secret exists in AWS parameter store if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) { // case: secret value doesn't match one in AWS parameter store // -> update secret await ssm.putParameter({ Name: `${integration.path}${key}`, Type: 'SecureString', Value: secrets[key], Overwrite: true }).promise(); } } }); // Identify secrets to delete Object.keys(awsParameterStoreSecretsObj).map(async (key) => { if (!(key in secrets)) { // case: // -> delete secret await ssm.deleteParameter({ Name: awsParameterStoreSecretsObj[key].Name }).promise(); } }); AWS.config.update({ region: undefined, accessKeyId: undefined, secretAccessKey: undefined }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error('Failed to sync secrets to AWS Parameter Store'); } } /** * Sync/push [secrets] to AWS secret manager * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessId - access id for AWS secret manager integration * @param {String} obj.accessToken - access token for AWS secret manager integration */ const syncSecretsAWSSecretManager = async ({ integration, secrets, accessId, accessToken }: { integration: IIntegration; secrets: any; accessId: string | null; accessToken: string; }) => { let secretsManager; try { if (!accessId) return; AWS.config.update({ region: integration.region, accessKeyId: accessId, secretAccessKey: accessToken }); secretsManager = new SecretsManagerClient({ region: integration.region, credentials: { accessKeyId: accessId, secretAccessKey: accessToken } }); const awsSecretManagerSecret = await secretsManager.send( new GetSecretValueCommand({ SecretId: integration.app }) ); let awsSecretManagerSecretObj: { [key: string]: any } = {}; if (awsSecretManagerSecret?.SecretString) { awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); } if (!_.isEqual(awsSecretManagerSecretObj, secrets)) { await secretsManager.send(new UpdateSecretCommand({ SecretId: integration.app, SecretString: JSON.stringify(secrets) })); } AWS.config.update({ region: undefined, accessKeyId: undefined, secretAccessKey: undefined }); } catch (err) { if (err instanceof ResourceNotFoundException && secretsManager) { await secretsManager.send(new CreateSecretCommand({ Name: integration.app, SecretString: JSON.stringify(secrets) })); } else { Sentry.setUser(null); Sentry.captureException(err); throw new Error('Failed to sync secrets to AWS Secret Manager'); } AWS.config.update({ region: undefined, accessKeyId: undefined, secretAccessKey: undefined }); } } /** * Sync/push [secrets] to Heroku app named [integration.app] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for Heroku integration */ const syncSecretsHeroku = async ({ integration, secrets, accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { const herokuSecrets = ( await request.get( `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, { headers: { Accept: "application/vnd.heroku+json; version=3", Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ) ).data; Object.keys(herokuSecrets).forEach((key) => { if (!(key in secrets)) { secrets[key] = null; } }); await request.patch( `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, secrets, { headers: { Accept: "application/vnd.heroku+json; version=3", Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to Heroku"); } }; /** * Sync/push [secrets] to Vercel project named [integration.app] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) */ const syncSecretsVercel = async ({ integration, integrationAuth, secrets, accessToken, }: { integration: IIntegration; integrationAuth: IIntegrationAuth; secrets: any; accessToken: string; }) => { interface VercelSecret { id?: string; type: string; key: string; value: string; target: string[]; } try { // Get all (decrypted) secrets back from Vercel in // decrypted format const params: { [key: string]: string } = { decrypt: "true", ...(integrationAuth?.teamId ? { teamId: integrationAuth.teamId, } : {}), }; // const res = ( // await Promise.all( // ( // await request.get( // `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, // { // params, // headers: { // Authorization: `Bearer ${accessToken}`, // 'Accept-Encoding': 'application/json' // } // } // )) // .data // .envs // .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)) // .map(async (secret: VercelSecret) => { // if (secret.type === 'encrypted') { // // case: secret is encrypted -> need to decrypt // const decryptedSecret = (await request.get( // `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, // { // params, // headers: { // Authorization: `Bearer ${accessToken}`, // 'Accept-Encoding': 'application/json' // } // } // )).data; // return decryptedSecret; // } // return secret; // }))).reduce((obj: any, secret: any) => ({ // ...obj, // [secret.key]: secret // }), {}); const vercelSecrets: VercelSecret[] = (await request.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, { params, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' } } )) .data .envs .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)); const res: { [key: string]: VercelSecret } = {}; for await (const vercelSecret of vercelSecrets) { if (vercelSecret.type === 'encrypted') { // case: secret is encrypted -> need to decrypt const decryptedSecret = (await request.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, { params, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' } } )).data; res[vercelSecret.key] = decryptedSecret; } else { res[vercelSecret.key] = vercelSecret; } } const updateSecrets: VercelSecret[] = []; const deleteSecrets: VercelSecret[] = []; const newSecrets: VercelSecret[] = []; // Identify secrets to create Object.keys(secrets).map((key) => { if (!(key in res)) { // case: secret has been created newSecrets.push({ key: key, value: secrets[key], type: "encrypted", target: [integration.targetEnvironment], }); } }); // Identify secrets to update and delete Object.keys(res).map((key) => { if (key in secrets) { if (res[key].value !== secrets[key]) { // case: secret value has changed updateSecrets.push({ id: res[key].id, key: key, value: secrets[key], type: res[key].type, target: res[key].target.includes(integration.targetEnvironment) ? [...res[key].target] : [...res[key].target, integration.targetEnvironment] }); } } else { // case: secret has been deleted deleteSecrets.push({ id: res[key].id, key: key, value: res[key].value, type: "encrypted", // value doesn't matter target: [integration.targetEnvironment], }); } }); // Sync/push new secrets if (newSecrets.length > 0) { await request.post( `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, newSecrets, { params, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); } for await (const secret of updateSecrets) { if (secret.type !== 'sensitive') { const { id, ...updatedSecret } = secret; await request.patch( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, updatedSecret, { params, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); } } for await (const secret of deleteSecrets) { await request.delete( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, { params, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to Vercel"); } }; /** * Sync/push [secrets] to Netlify site with id [integration.appId] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {Object} obj.accessToken - access token for Netlify integration */ const syncSecretsNetlify = async ({ integration, integrationAuth, secrets, accessToken, }: { integration: IIntegration; integrationAuth: IIntegrationAuth; secrets: any; accessToken: string; }) => { try { interface NetlifyValue { id?: string; context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', value: string; } interface NetlifySecret { key: string; values: NetlifyValue[]; } interface NetlifySecretsRes { [index: string]: NetlifySecret; } const getParams = new URLSearchParams({ context_name: "all", // integration.context or all site_id: integration.appId, }); const res = ( await request.get( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, { params: getParams, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ) ).data.reduce( (obj: any, secret: any) => ({ ...obj, [secret.key]: secret, }), {} ); const newSecrets: NetlifySecret[] = []; // createEnvVars const deleteSecrets: string[] = []; // deleteEnvVar const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue const updateSecrets: NetlifySecret[] = []; // setEnvVarValue // identify secrets to create and update Object.keys(secrets).map((key) => { if (!(key in res)) { // case: Infisical secret does not exist in Netlify -> create secret newSecrets.push({ key, values: [ { value: secrets[key], context: integration.targetEnvironment, }, ], }); } else { // case: Infisical secret exists in Netlify const contexts = res[key].values.reduce( (obj: any, value: NetlifyValue) => ({ ...obj, [value.context]: value, }), {} ); if (integration.targetEnvironment in contexts) { // case: Netlify secret value exists in integration context if (secrets[key] !== contexts[integration.targetEnvironment].value) { // case: Infisical and Netlify secret values are different // -> update Netlify secret context and value updateSecrets.push({ key, values: [ { context: integration.targetEnvironment, value: secrets[key], }, ], }); } } else { // case: Netlify secret value does not exist in integration context // -> add the new Netlify secret context and value updateSecrets.push({ key, values: [ { context: integration.targetEnvironment, value: secrets[key], }, ], }); } } }); // identify secrets to delete // TODO: revise (patch case where 1 context was deleted but others still there Object.keys(res).map((key) => { // loop through each key's context if (!(key in secrets)) { // case: Netlify secret does not exist in Infisical const numberOfValues = res[key].values.length; res[key].values.forEach((value: NetlifyValue) => { if (value.context === integration.targetEnvironment) { if (numberOfValues <= 1) { // case: Netlify secret value has less than 1 context -> delete secret deleteSecrets.push(key); } else { // case: Netlify secret value has more than 1 context -> delete secret value context deleteSecretValues.push({ key, values: [ { id: value.id, context: integration.targetEnvironment, value: value.value, }, ], }); } } }); } }); const syncParams = new URLSearchParams({ site_id: integration.appId, }); if (newSecrets.length > 0) { await request.post( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, newSecrets, { params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); } if (updateSecrets.length > 0) { updateSecrets.forEach(async (secret: NetlifySecret) => { await request.patch( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, { context: secret.values[0].context, value: secret.values[0].value, }, { params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); }); } if (deleteSecrets.length > 0) { deleteSecrets.forEach(async (key: string) => { await request.delete( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, { params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); }); } if (deleteSecretValues.length > 0) { deleteSecretValues.forEach(async (secret: NetlifySecret) => { await request.delete( `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, { params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); }); } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to Heroku"); } }; /** * Sync/push [secrets] to GitHub repo with name [integration.app] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for GitHub integration */ const syncSecretsGitHub = async ({ integration, secrets, accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { interface GitHubRepoKey { key_id: string; key: string; } interface GitHubSecret { name: string; created_at: string; updated_at: string; } interface GitHubSecretRes { [index: string]: GitHubSecret; } const deleteSecrets: GitHubSecret[] = []; const octokit = new Octokit({ auth: accessToken, }); // const user = (await octokit.request('GET /user', {})).data; const repoPublicKey: GitHubRepoKey = ( await octokit.request( "GET /repos/{owner}/{repo}/actions/secrets/public-key", { owner: integration.owner, repo: integration.app, } ) ).data; // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key const encryptedSecrets: GitHubSecretRes = ( await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { owner: integration.owner, repo: integration.app, }) ).data.secrets.reduce( (obj: any, secret: any) => ({ ...obj, [secret.name]: secret, }), {} ); Object.keys(encryptedSecrets).map(async (key) => { if (!(key in secrets)) { await octokit.request( "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { owner: integration.owner, repo: integration.app, secret_name: key, } ); } }); Object.keys(secrets).map((key) => { // let encryptedSecret; sodium.ready.then(async () => { // convert secret & base64 key to Uint8Array. const binkey = sodium.from_base64( repoPublicKey.key, sodium.base64_variants.ORIGINAL ); const binsec = sodium.from_string(secrets[key]); // encrypt secret using libsodium const encBytes = sodium.crypto_box_seal(binsec, binkey); // convert encrypted Uint8Array to base64 const encryptedSecret = sodium.to_base64( encBytes, sodium.base64_variants.ORIGINAL ); await octokit.request( "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { owner: integration.owner, repo: integration.app, secret_name: key, encrypted_value: encryptedSecret, key_id: repoPublicKey.key_id, } ); }); }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to GitHub"); } }; /** * Sync/push [secrets] to Render service with id [integration.appId] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for Render integration */ const syncSecretsRender = async ({ integration, secrets, accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { await request.put( `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, Object.keys(secrets).map((key) => ({ key, value: secrets[key], })), { headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'application/json' }, } ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to Render"); } }; /** * Sync/push [secrets] to Fly.io app * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for Render integration */ const syncSecretsFlyio = async ({ integration, secrets, accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { // set secrets const SetSecrets = ` mutation($input: SetSecretsInput!) { setSecrets(input: $input) { release { id version reason description user { id email name } evaluationId createdAt } } } `; await request.post(INTEGRATION_FLYIO_API_URL, { query: SetSecrets, variables: { input: { appId: integration.app, secrets: Object.entries(secrets).map(([key, value]) => ({ key, value, })), }, }, }, { headers: { Authorization: "Bearer " + accessToken, 'Accept-Encoding': 'application/json', }, }); // get secrets interface FlyioSecret { name: string; digest: string; createdAt: string; } const GetSecrets = `query ($appName: String!) { app(name: $appName) { secrets { name digest createdAt } } }`; const getSecretsRes = (await request.post(INTEGRATION_FLYIO_API_URL, { query: GetSecrets, variables: { appName: integration.app, }, }, { headers: { Authorization: "Bearer " + accessToken, 'Content-Type': 'application/json', 'Accept-Encoding': 'application/json', }, })).data.data.app.secrets; const deleteSecretsKeys = getSecretsRes .filter((secret: FlyioSecret) => !(secret.name in secrets)) .map((secret: FlyioSecret) => secret.name); // unset (delete) secrets const DeleteSecrets = `mutation($input: UnsetSecretsInput!) { unsetSecrets(input: $input) { release { id version reason description user { id email name } evaluationId createdAt } } }`; await request.post(INTEGRATION_FLYIO_API_URL, { query: DeleteSecrets, variables: { input: { appId: integration.app, keys: deleteSecretsKeys, }, }, }, { headers: { Authorization: "Bearer " + accessToken, "Content-Type": "application/json", 'Accept-Encoding': 'application/json', }, }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to Fly.io"); } }; /** * Sync/push [secrets] to CircleCI project * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for CircleCI integration */ const syncSecretsCircleCI = async ({ integration, secrets, accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { const circleciOrganizationDetail = ( await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { headers: { "Circle-Token": accessToken, "Accept-Encoding": "application/json", }, }) ).data[0]; const { slug } = circleciOrganizationDetail; // sync secrets to CircleCI Object.keys(secrets).forEach( async (key) => await request.post( `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, { name: key, value: secrets[key], }, { headers: { "Circle-Token": accessToken, "Content-Type": "application/json", }, } ) ); // get secrets from CircleCI const getSecretsRes = ( await request.get( `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, { headers: { "Circle-Token": accessToken, "Accept-Encoding": "application/json", }, } ) ).data?.items; // delete secrets from CircleCI getSecretsRes.forEach(async (sec: any) => { if (!(sec.name in secrets)) { await request.delete( `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`, { headers: { "Circle-Token": accessToken, "Content-Type": "application/json", }, } ); } }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to CircleCI"); } }; /** * Sync/push [secrets] to TravisCI project * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for TravisCI integration */ const syncSecretsTravisCI = async ({ integration, secrets, accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { // get secrets from travis-ci const getSecretsRes = ( await request.get( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, { headers: { "Authorization": `token ${accessToken}`, "Accept-Encoding": "application/json", }, } ) ) .data ?.env_vars .reduce((obj: any, secret: any) => ({ ...obj, [secret.name]: secret }), {}); // add secrets for await (const key of Object.keys(secrets)) { if (!(key in getSecretsRes)) { // case: secret does not exist in travis ci // -> add secret await request.post( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, { env_var: { name: key, value: secrets[key] } }, { headers: { "Authorization": `token ${accessToken}`, "Content-Type": "application/json", "Accept-Encoding": "application/json", }, } ); } else { // case: secret exists in travis ci // -> update/set secret await request.patch( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, { env_var: { name: key, value: secrets[key], } }, { headers: { "Authorization": `token ${accessToken}`, "Content-Type": "application/json", "Accept-Encoding": "application/json", }, } ); } } for await (const key of Object.keys(getSecretsRes)) { if (!(key in secrets)){ // delete secret await request.delete( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, { headers: { "Authorization": `token ${accessToken}`, "Content-Type": "application/json", "Accept-Encoding": "application/json", }, } ); } } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to GitLab"); } } /** * Sync/push [secrets] to GitLab repo with name [integration.app] * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {IIntegrationAuth} obj.integrationAuth - integration auth details * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {String} obj.accessToken - access token for GitLab integration */ const syncSecretsGitLab = async ({ integration, secrets, accessToken, }: { integration: IIntegration; secrets: any; accessToken: string; }) => { try { // get secrets from gitlab const getSecretsRes = ( await request.get( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, { headers: { "Authorization": `Bearer ${accessToken}`, "Accept-Encoding": "application/json", }, } ) ).data; for await (const key of Object.keys(secrets)) { const existingSecret = getSecretsRes.find((s: any) => s.key == key); if (!existingSecret) { await request.post( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, { key: key, value: secrets[key], protected: false, masked: false, raw: false, environment_scope:'*' }, { headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", "Accept-Encoding": "application/json", }, } ) } else { // udpate secret await request.put( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}`, { ...existingSecret, value: secrets[existingSecret.key] }, { headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", "Accept-Encoding": "application/json", }, } ) } } // delete secrets for await (const sec of getSecretsRes) { if (!(sec.key in secrets)) { await request.delete( `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}`, { headers: { "Authorization": `Bearer ${accessToken}`, }, } ); } } }catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error("Failed to sync secrets to GitLab"); } } export { syncSecrets };