Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-07-21 14:37:13 +07:00
136 changed files with 5831 additions and 16909 deletions
-30
View File
@@ -105,36 +105,6 @@ jobs:
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
secret-scanning-git-app:
name: Build secret scanning git app
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 🏗️ Build secret scanning git app and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: secret-engine
tags: |
infisical/staging_deployment_secret-scanning-git-app:${{ steps.commit.outputs.short }}
infisical/staging_deployment_secret-scanning-git-app:latest
platforms: linux/amd64,linux/arm64
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
+1 -7
View File
@@ -10,7 +10,6 @@
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-empty-function": "off",
"no-console": 2,
"quotes": [
"error",
@@ -34,11 +33,6 @@
"argsIgnorePattern": "^_"
}
],
"sort-imports": [
"error",
{
"ignoreDeclarationSort": true
}
]
"sort-imports": 1
}
}
+4
View File
@@ -19,6 +19,10 @@ RUN npm ci --only-production
COPY --from=build /app .
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
+2930 -290
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -37,6 +37,7 @@
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
"query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2",
@@ -104,6 +105,7 @@
"jest-junit": "^15.0.0",
"nodemon": "^2.0.19",
"npm": "^8.19.3",
"smee-client": "^1.2.3",
"supertest": "^6.3.3",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1"
+6 -1
View File
@@ -10,7 +10,7 @@ export const getEncryptionKey = async () => {
return secretValue === "" ? undefined : secretValue;
}
export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
@@ -57,6 +57,11 @@ export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWOR
export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
export const getSecretScanningWebhookProxy = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
export const getSecretScanningWebhookSecret = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
export const getSecretScanningGitAppId = async () => (await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
export const getSecretScanningPrivateKey = async () => (await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
+3 -1
View File
@@ -15,6 +15,7 @@ import * as userController from "./userController";
import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController";
export {
authController,
@@ -33,5 +34,6 @@ export {
userController,
workspaceController,
secretScanningController,
webhookController
webhookController,
secretImportController
};
@@ -0,0 +1,117 @@
import { Request, Response } from "express";
import { validateMembership } from "../../helpers";
import SecretImport from "../../models/secretImports";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { BadRequestError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
export const createSecretImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId, secretImport } = req.body;
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
const doc = new SecretImport({
workspace: workspaceId,
environment,
folderId,
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
});
await doc.save();
return res.status(200).json({ message: "successfully created secret import" });
}
const doesImportExist = importSecDoc.imports.find(
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
);
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath
});
await importSecDoc.save();
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
export const updateSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImports } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
importSecDoc.imports = secretImports;
await importSecDoc.save();
return res.status(200).json({ message: "successfully updated secret import" });
};
export const deleteSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImportEnv, secretImportPath } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
return res.status(200).json({ message: "successfully delete secret import" });
};
export const getSecretImports = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query;
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
return res.status(200).json({ secretImport: importSecDoc });
};
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query as {
workspaceId: string;
environment: string;
folderId: string;
};
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: {} });
}
const secrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).json({ secrets });
};
@@ -5,7 +5,7 @@ import { Types } from "mongoose";
import { UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
import { MembershipOrg } from "../../models";
import GitRisks, { STATUS_UNRESOLVED } from "../../models/gitRisks";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../models/gitRisks";
export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex");
@@ -72,7 +72,7 @@ export const getCurrentOrganizationInstallationStatus = async (req: Request, res
export const getRisksForOrganization = async (req: Request, res: Response) => {
const { organizationId } = req.params
const risks = await GitRisks.find({ organization: organizationId, status: STATUS_UNRESOLVED }).lean()
const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean()
res.json({
risks: risks
})
@@ -81,9 +81,11 @@ export const getRisksForOrganization = async (req: Request, res: Response) => {
export const updateRisksStatus = async (req: Request, res: Response) => {
const { riskId } = req.params
const { status } = req.body
const risks = await GitRisks.findByIdAndUpdate(riskId, {
sttaus: status
const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false
const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status,
isResolved: isRiskResolved
}).lean()
res.json(risks)
res.json(risk)
}
@@ -27,16 +27,16 @@ export const createWorkspaceEnvironment = async (
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization.toString());
if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed
if (workspace.environments.length >= plan.environmentLimit) {
// case: number of environments used exceeds the number of environments allowed
return res.status(400).send({
message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.",
});
@@ -191,14 +191,21 @@ export const deleteWorkspaceEnvironment = async (
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
// await ServiceToken.deleteMany({
// workspace: workspaceId,
// environment: environmentSlug,
// });
const result = await ServiceTokenData.updateMany(
{ workspace: workspaceId },
{ $pull: { scopes: { environment: environmentSlug } } }
);
if (result.modifiedCount > 0) {
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
}
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
@@ -35,6 +35,7 @@ import {
} from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets";
import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService";
/**
* Peform a batch of any specified CUD secret operations
@@ -690,7 +691,7 @@ export const getSecrets = async (req: Request, res: Response) => {
}
*/
const { tagSlugs, secretPath } = req.query;
const { tagSlugs, secretPath, include_imports } = req.query;
let { folderId } = req.query;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
@@ -827,6 +828,12 @@ export const getSecrets = async (req: Request, res: Response) => {
secrets = await Secret.find(secretQuery).populate("tags");
}
// TODO(akhilmhdh) - secret-imp change this to org type
let importedSecrets: any[] = [];
if (include_imports) {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
}
const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const readAction = await EELogService.createAction({
@@ -868,7 +875,8 @@ export const getSecrets = async (req: Request, res: Response) => {
}
return res.status(200).send({
secrets
secrets,
...(include_imports && { imports: importedSecrets })
});
};
@@ -80,6 +80,26 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
});
}
/**
* Update the current user's name [firstName, lastName].
* @param req
* @param res
* @returns
*/
export const updateName = async (req: Request, res: Response) => {
const { firstName, lastName }: { firstName: string; lastName: string; } = req.body;
req.user.firstName = firstName;
req.user.lastName = lastName || "";
await req.user.save();
const user = req.user;
return res.status(200).send({
user,
});
}
/**
* Return organizations that the current user is part of.
* @param req
@@ -5,6 +5,10 @@ import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
/**
* Return secrets for workspace with id [workspaceId] and environment
@@ -16,6 +20,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@@ -28,13 +33,38 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId)
});
if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).send({
secrets: secrets.map((secret) =>
repackageSecretToRaw({
secret,
key
})
),
imports: importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((secret) => repackageSecretToRaw({ secret, key }))
}))
});
}
return res.status(200).send({
secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({
secret,
key
});
return rep;
})
});
@@ -232,6 +262,7 @@ export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@@ -240,6 +271,24 @@ export const getSecrets = async (req: Request, res: Response) => {
authData: req.authData
});
if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).send({
secrets,
imports: importedSecrets
});
}
return res.status(200).send({
secrets
});
+28 -3
View File
@@ -5,11 +5,12 @@ import express from "express";
require("express-async-errors");
import helmet from "helmet";
import cors from "cors";
import { DatabaseService } from "./services";
import { DatabaseService, GithubSecretScanningService } from "./services";
import { EELicenseService } from "./ee/services";
import { setUpHealthEndpoint } from "./services/health";
import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express");
import { Probot, createNodeMiddleware } from "probot";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -42,7 +43,8 @@ import {
userAction as v1UserActionRouter,
user as v1UserRouter,
webhooks as v1WebhooksRouter,
workspace as v1WorkspaceRouter
workspace as v1WorkspaceRouter,
secretImport as v1SecretImportRouter
} from "./routes/v1";
import {
auth as v2AuthRouter,
@@ -67,10 +69,12 @@ import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler";
import { getNodeEnv, getPort, getSiteURL } from "./config";
import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config";
import { setup } from "./utils/setup";
const SmeeClient = require('smee-client') // eslint-disable-line
const main = async () => {
await setup();
await EELicenseService.initGlobalFeatureSet();
@@ -87,6 +91,26 @@ const main = async () => {
})
);
if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) {
const probot = new Probot({
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
secret: await getSecretScanningWebhookSecret(),
});
if ((await getNodeEnv()) != "production") {
const smee = new SmeeClient({
source: await getSecretScanningWebhookProxy(),
target: "http://backend:4000/ss-webhook",
logger: console
})
smee.start()
}
app.use(createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })); // secret scanning webhook
}
if ((await getNodeEnv()) === "production") {
// enable app-wide rate-limiting + helmet security
// in production
@@ -131,6 +155,7 @@ const main = async () => {
app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter);
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);
+64 -25
View File
@@ -32,6 +32,9 @@ import {
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL
} from "../variables";
interface App {
@@ -145,6 +148,11 @@ const getApps = async ({
accountId: accessId
})
break;
case INTEGRATION_CODEFRESH:
apps = await getAppsCodefresh({
accessToken,
});
break;
}
return apps;
@@ -196,10 +204,10 @@ const getAppsVercel = async ({
},
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId,
},
}
params: {
teamId: integrationAuth.teamId,
},
}
: {}),
})
).data;
@@ -695,30 +703,61 @@ const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - Cloudflare Pages projects
* @returns {String} apps.name - name of Cloudflare Pages project
*/
const getAppsCloudflarePages = async ({
accessToken,
accountId
const getAppsCloudflarePages = async ({
accessToken,
accountId
}: {
accessToken: string;
accountId?: string;
accessToken: string;
accountId?: string;
}) => {
const { data } = await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
const { data } = await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
const apps = data.result.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
const apps = data.result.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
}
/**
* Return list of projects for Supabase integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Supabase API
* @returns {Object[]} apps - names of Supabase apps
* @returns {String} apps.name - name of Supabase app
*/
const getAppsCodefresh = async ({
accessToken,
}: {
accessToken: string;
}) => {
const res = (
await standardRequest.get(`${INTEGRATION_CODEFRESH_API_URL}/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res.projects.map((a: any) => ({
name: a.projectName,
appId: a.id,
}));
return apps;
};
export { getApps };
+230 -160
View File
@@ -1,7 +1,7 @@
import _ from "lodash";
import AWS from "aws-sdk";
import {
CreateSecretCommand,
import {
CreateSecretCommand,
GetSecretValueCommand,
ResourceNotFoundException,
SecretsManagerClient,
@@ -42,8 +42,10 @@ import {
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL
} from "../variables";
import { standardRequest} from "../config/request";
import { standardRequest } from "../config/request";
/**
* Sync/push [secrets] to [app] in integration named [integration]
@@ -173,11 +175,39 @@ const syncSecrets = async ({
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken,
});
break;
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;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CHECKLY:
await syncSecretsCheckly({
integration,
@@ -196,10 +226,17 @@ const syncSecrets = async ({
break;
case INTEGRATION_CLOUDFLARE_PAGES:
await syncSecretsCloudflarePages({
integration,
secrets,
accessId,
accessToken
integration,
secrets,
accessId,
accessToken
});
break;
case INTEGRATION_CODEFRESH:
await syncSecretsCodefresh({
integration,
secrets,
accessToken,
});
break;
}
@@ -231,11 +268,11 @@ const syncSecretsAzureKeyVault = async ({
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
@@ -249,23 +286,23 @@ const syncSecretsAzureKeyVault = async ({
Authorization: `Bearer ${accessToken}`,
},
});
result = result.concat(res.data.value);
url = res.data.nextLink;
}
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 standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
headers: {
"Authorization": `Bearer ${accessToken}`,
@@ -277,11 +314,11 @@ const syncSecretsAzureKeyVault = async ({
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1),
});
})))
.reduce((obj: any, secret: any) => ({
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret,
}), {});
}), {});
const setSecrets: {
key: string;
value: string;
@@ -305,9 +342,9 @@ const syncSecretsAzureKeyVault = async ({
}
}
});
const deleteSecrets: AzureKeyVaultSecret[] = [];
Object.keys(res).forEach((key) => {
const underscoredKey = key.replace(/-/g, "_");
if (!(underscoredKey in secrets)) {
@@ -328,7 +365,7 @@ const syncSecretsAzureKeyVault = async ({
}) => {
let isSecretSet = false;
let maxTries = 6;
while (!isSecretSet && maxTries > 0) {
// try to set secret
try {
@@ -345,7 +382,7 @@ const syncSecretsAzureKeyVault = async ({
);
isSecretSet = true;
} catch (err) {
const error: any = err;
if (error?.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") {
@@ -365,7 +402,7 @@ const syncSecretsAzureKeyVault = async ({
}
}
}
// Sync/push set secrets
for await (const setSecret of setSecrets) {
const { key, value } = setSecret;
@@ -376,7 +413,7 @@ const syncSecretsAzureKeyVault = async ({
accessToken,
});
}
for await (const deleteSecret of deleteSecrets) {
const { key } = deleteSecret;
await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
@@ -418,7 +455,7 @@ const syncSecretsAWSParameterStore = async ({
apiVersion: "2014-11-06",
region: integration.region,
});
const params = {
Path: integration.path,
Recursive: true,
@@ -426,61 +463,61 @@ const syncSecretsAWSParameterStore = async ({
};
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,
...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
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();
} 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();
}
if (!(key in secrets)) {
// case:
// -> delete secret
await ssm.deleteParameter({
Name: awsParameterStoreSecretsObj[key].Name,
}).promise();
}
});
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined,
});
});
}
/**
@@ -511,7 +548,7 @@ const syncSecretsAWSSecretManager = async ({
accessKeyId: accessId,
secretAccessKey: accessToken,
});
secretsManager = new SecretsManagerClient({
region: integration.region,
credentials: {
@@ -525,13 +562,13 @@ const syncSecretsAWSSecretManager = async ({
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,
@@ -543,19 +580,19 @@ const syncSecretsAWSSecretManager = async ({
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),
}));
}
}
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined,
});
});
}
}
@@ -638,36 +675,36 @@ const syncSecretsVercel = async ({
decrypt: "true",
...(integrationAuth?.teamId
? {
teamId: integrationAuth.teamId,
}
teamId: integrationAuth.teamId,
}
: {}),
};
const vercelSecrets: VercelSecret[] = (await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
))
.data
.envs
.filter((secret: VercelSecret) => {
if (!secret.target.includes(integration.targetEnvironment)) {
// case: secret does not have the same target environment
return false;
}
.data
.envs
.filter((secret: VercelSecret) => {
if (!secret.target.includes(integration.targetEnvironment)) {
// case: secret does not have the same target environment
return false;
}
if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) {
// case: secret on preview environment does not have same target git branch
return false;
}
if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) {
// case: secret on preview environment does not have same target git branch
return false;
}
return true;
});
return true;
});
// return secret.target.includes(integration.targetEnvironment);
@@ -677,14 +714,14 @@ const syncSecretsVercel = async ({
if (vercelSecret.type === "encrypted") {
// case: secret is encrypted -> need to decrypt
const decryptedSecret = (await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
`${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;
@@ -692,7 +729,7 @@ const syncSecretsVercel = async ({
res[vercelSecret.key] = vercelSecret;
}
}
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
@@ -723,9 +760,9 @@ const syncSecretsVercel = async ({
key: key,
value: secrets[key],
type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment],
target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path,
} : {}),
@@ -775,7 +812,7 @@ const syncSecretsVercel = async ({
},
}
);
}
}
}
for await (const secret of deleteSecrets) {
@@ -788,7 +825,7 @@ const syncSecretsVercel = async ({
"Accept-Encoding": "application/json",
},
}
);
);
}
};
@@ -1375,7 +1412,7 @@ const syncSecretsCircleCI = async ({
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
}) => {
const circleciOrganizationDetail = (
await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: {
@@ -1462,13 +1499,13 @@ const syncSecretsTravisCI = async ({
}
)
)
.data
?.env_vars
.reduce((obj: any, secret: any) => ({
.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)) {
@@ -1513,7 +1550,7 @@ const syncSecretsTravisCI = async ({
}
for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)){
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`,
@@ -1558,29 +1595,29 @@ const syncSecretsGitLab = async ({
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
};
let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}?per_page=100`;
while (url) {
const response: any = await standardRequest.get(url, { headers });
allEnvVariables = [...allEnvVariables, ...response.data];
const linkHeader = response.headers.link;
const nextLink = linkHeader?.split(",").find((part: string) => part.includes('rel="next"'));
if (nextLink) {
url = nextLink.trim().split(";")[0].slice(1, -1);
} else {
url = null;
}
}
return allEnvVariables;
};
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
secret.environment_scope === integration.targetEnvironment
);
@@ -1662,8 +1699,8 @@ const syncSecretsSupabase = async ({
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
@@ -1672,8 +1709,8 @@ const syncSecretsSupabase = async ({
const modifiedFormatForSecretInjection = Object.keys(secrets).map(
(key) => {
return {
name: key,
value: secrets[key],
name: key,
value: secrets[key],
};
}
);
@@ -1683,8 +1720,8 @@ const syncSecretsSupabase = async ({
modifiedFormatForSecretInjection,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
@@ -1692,7 +1729,7 @@ const syncSecretsSupabase = async ({
const secretsToDelete: any = [];
getSecretsRes?.forEach((secretObj: any) => {
if (!(secretObj.name in secrets)) {
secretsToDelete.push(secretObj.name);
secretsToDelete.push(secretObj.name);
}
});
@@ -1739,18 +1776,18 @@ const syncSecretsCheckly = async ({
}
)
)
.data
.reduce((obj: any, secret: any) => ({
.data
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret.value,
}), {});
}), {});
// add secrets
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {
// case: secret does not exist in checkly
// -> add secret
await standardRequest.post(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables`,
{
@@ -1769,7 +1806,7 @@ const syncSecretsCheckly = async ({
} else {
// case: secret exists in checkly
// -> update/set secret
if (secrets[key] !== getSecretsRes[key]) {
await standardRequest.put(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`,
@@ -1790,7 +1827,7 @@ const syncSecretsCheckly = async ({
}
for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)){
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`,
@@ -1827,13 +1864,13 @@ const syncSecretsHashiCorpVault = async ({
accessToken: string;
}) => {
if (!accessId) return;
interface LoginAppRoleRes {
auth: {
client_token: string;
}
}
// get Vault client token (could be optimized)
const { data }: { data: LoginAppRoleRes } = await standardRequest.post(
`${integrationAuth.url}/v1/auth/approle/login`,
@@ -1847,7 +1884,7 @@ const syncSecretsHashiCorpVault = async ({
},
}
);
const clientToken = data.auth.client_token;
await standardRequest.post(
@@ -1875,46 +1912,46 @@ const syncSecretsHashiCorpVault = async ({
* @param {String} obj.accessToken - API token for Cloudflare
*/
const syncSecretsCloudflarePages = async ({
integration,
secrets,
accessId,
accessToken,
integration,
secrets,
accessId,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
// get secrets from cloudflare pages
const getSecretsRes = (
await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
)
await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
)
)
.data.result["deployment_configs"][integration.targetEnvironment]["env_vars"];
.data.result['deployment_configs'][integration.targetEnvironment]['env_vars'];
// copy the secrets object, so we can set deleted keys to null
const secretsObj: any = {...secrets};
const secretsObj: any = { ...secrets };
for (const [key, val] of Object.entries(secretsObj)) {
secretsObj[key] = { type: "secret_text", value: val };
secretsObj[key] = { type: "secret_text", value: val };
}
if (getSecretsRes) {
for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)) {
// case: secret does not exist in infisical
// -> delete secret from cloudflare pages
secretsObj[key] = null;
}
for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)) {
// case: secret does not exist in infisical
// -> delete secret from cloudflare pages
secretsObj[key] = null;
}
}
}
const data = {
@@ -1926,15 +1963,48 @@ const syncSecretsCloudflarePages = async ({
};
await standardRequest.patch(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`,
data,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`,
data,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
}
/**
* Sync/push [secrets] to Codefresh with name [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 Codefresh integration
*/
const syncSecretsCodefresh = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
await standardRequest.patch(
`${INTEGRATION_CODEFRESH_API_URL}/projects/${integration.appId}`,
{
variables: Object.keys(secrets).map((key) => ({
key,
value: secrets[key]
}))
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
};
export { syncSecrets };
+4
View File
@@ -25,6 +25,7 @@ export type GitRisks = {
tags: string[];
ruleID: string;
fingerprint: string;
fingerPrintWithoutCommitId: string
isFalsePositive: boolean; // New field for marking risks as false positives
isResolved: boolean; // New field for marking risks as resolved
@@ -94,6 +95,9 @@ const gitRisks = new Schema<GitRisks>({
type: String,
unique: true
},
fingerPrintWithoutCommitId: {
type: String,
},
isFalsePositive: {
type: Boolean,
default: false
+4 -1
View File
@@ -17,13 +17,14 @@ import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; //
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
import TokenData, { ITokenData } from "./tokenData";
import User,{ AuthProvider, IUser } from "./user";
import User, { AuthProvider, IUser } from "./user";
import UserAction, { IUserAction } from "./userAction";
import Workspace, { IWorkspace } from "./workspace";
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
import APIKeyData, { IAPIKeyData } from "./apiKeyData";
import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
import TokenVersion, { ITokenVersion } from "./tokenVersion";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./gitRisks";
export {
AuthProvider,
@@ -79,4 +80,6 @@ export {
ILoginSRPDetail,
TokenVersion,
ITokenVersion,
GitRisks,
STATUS_RESOLVED_FALSE_POSITIVE
};
+4 -1
View File
@@ -18,6 +18,7 @@ import {
INTEGRATION_SUPABASE,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_CODEFRESH
} from "../variables";
export interface IIntegration {
@@ -54,7 +55,8 @@ export interface IIntegration {
| "supabase"
| "checkly"
| "hashicorp-vault"
| "cloudflare-pages";
| "cloudflare-pages"
| "codefresh";
integrationAuth: Types.ObjectId;
}
@@ -144,6 +146,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH
],
required: true,
},
+4 -2
View File
@@ -19,13 +19,14 @@ import {
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL
INTEGRATION_VERCEL,
INTEGRATION_CODEFRESH
} from "../variables";
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "checkly" | "cloudflare-pages";
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'laravel-forge' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly' | 'cloudflare-pages' | 'codefresh';
teamId: string;
accountId: string;
url: string;
@@ -71,6 +72,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH
],
required: true,
},
+52
View File
@@ -0,0 +1,52 @@
import { Schema, Types, model } from "mongoose";
export interface ISecretImports {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
folderId: string;
imports: Array<{
environment: string;
secretPath: string;
}>;
}
const secretImportSchema = new Schema<ISecretImports>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
folderId: {
type: String,
required: true,
default: "root"
},
imports: {
type: [
{
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true
}
}
],
default: []
}
},
{
timestamps: true
}
);
const SecretImport = model<ISecretImports>("SecretImports", secretImportSchema);
export default SecretImport;
+2 -1
View File
@@ -1,5 +1,5 @@
import express, { Request, Response } from "express";
import { getSmtpConfigured } from "../../config";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
const router = express.Router();
@@ -10,6 +10,7 @@ router.get(
date: new Date(),
message: "Ok",
emailConfigured: await getSmtpConfigured(),
secretScanningConfigured: await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey(),
})
}
);
+3 -1
View File
@@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning";
import webhooks from "./webhook";
import secretImport from "./secretImport";
export {
signup,
@@ -37,5 +38,6 @@ export {
integrationAuth,
secretsFolder,
secretScanning,
webhooks
webhooks,
secretImport
};
+84
View File
@@ -0,0 +1,84 @@
import express from "express";
const router = express.Router();
import { body, param, query } from "express-validator";
import { secretImportController } from "../../controllers/v1";
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body"
}),
body("workspaceId").exists().isString().trim().notEmpty(),
body("environment").exists().isString().trim().notEmpty(),
body("folderId").default("root").isString().trim(),
body("secretImport").exists().isObject(),
body("secretImport.environment").isString().exists().trim(),
body("secretImport.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.createSecretImport
);
router.put(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImports").exists().isArray(),
body("secretImports.*.environment").isString().exists().trim(),
body("secretImports.*.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.updateSecretImport
);
router.delete(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImportPath").isString().exists().trim(),
body("secretImportEnv").isString().exists().trim(),
validateRequest,
secretImportController.updateSecretImport
);
router.get(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query"
}),
query("workspaceId").exists().isString().trim().notEmpty(),
query("environment").exists().isString().trim().notEmpty(),
query("folderId").default("root").isString().trim(),
validateRequest,
secretImportController.getSecretImports
);
router.get(
"/secrets",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query"
}),
query("workspaceId").exists().isString().trim().notEmpty(),
query("environment").exists().isString().trim().notEmpty(),
query("folderId").default("root").isString().trim(),
validateRequest,
secretImportController.getAllSecretsFromImport
);
export default router;
+1
View File
@@ -69,6 +69,7 @@ router.post(
}),
param("organizationId").exists().trim(),
param("riskId").exists().trim(),
body("status").exists(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
+1
View File
@@ -127,6 +127,7 @@ router.get(
query("tagSlugs"),
query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(),
query("include_imports").optional().default(false).isBoolean(),
validateRequest,
requireAuth({
acceptedAuthModes: [
+10
View File
@@ -29,6 +29,16 @@ router.patch(
usersController.updateMyMfaEnabled
);
router.patch(
"/me/name",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("firstName").exists(),
validateRequest,
usersController.updateName
);
router.get(
"/me/organizations",
requireAuth({
+33 -36
View File
@@ -1,10 +1,6 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator";
import { secretsController } from "../../controllers/v3";
import {
@@ -17,7 +13,7 @@ import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL,
SECRET_SHARED,
SECRET_SHARED
} from "../../variables";
router.get(
@@ -25,14 +21,15 @@ router.get(
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(),
query("include_imports").optional().isBoolean().default(false),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -40,7 +37,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.getSecretsRaw
);
@@ -58,8 +55,8 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -67,7 +64,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.getSecretByNameRaw
);
@@ -86,8 +83,8 @@ router.post(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -95,7 +92,7 @@ router.post(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.createSecretRaw
);
@@ -114,8 +111,8 @@ router.patch(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -123,7 +120,7 @@ router.patch(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.updateSecretByNameRaw
);
@@ -141,8 +138,8 @@ router.delete(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -150,7 +147,7 @@ router.delete(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.deleteSecretByNameRaw
);
@@ -166,8 +163,8 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -175,7 +172,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
requireE2EEOff: false
}),
secretsController.getSecrets
);
@@ -201,8 +198,8 @@ router.post(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -210,7 +207,7 @@ router.post(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
requireE2EEOff: false
}),
secretsController.createSecret
);
@@ -228,15 +225,15 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireBlindIndicesEnabled: true
}),
secretsController.getSecretByName
);
@@ -257,8 +254,8 @@ router.patch(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -266,7 +263,7 @@ router.patch(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
requireE2EEOff: false
}),
secretsController.updateSecretByName
);
@@ -284,8 +281,8 @@ router.delete(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@@ -293,7 +290,7 @@ router.delete(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
requireE2EEOff: false
}),
secretsController.deleteSecretByName
);
@@ -1,15 +1,15 @@
import { Probot } from "probot";
import { exec } from "child_process";
import { writeFile, readFile, rm, mkdir, } from "fs";
import { mkdir, readFile, rm, writeFile } from "fs";
import { tmpdir } from "os";
import { join } from "path"
import mongoose from "mongoose";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./models/gitRisks";
import GitAppOrganizationInstallation from "./models/gitAppOrganizationInstallation";
import { sendMail, setTransporter } from "./helper/nodemailer";
import { initSmtp } from "./service/smtp";
import MembershipOrg, { ADMIN, OWNER } from "./models/membershipOrg";
import User from "./models/user";
import GitRisks from "../models/gitRisks";
import GitAppOrganizationInstallation from "../models/gitAppOrganizationInstallation";
import MembershipOrg from "../models/membershipOrg";
import { ADMIN, OWNER } from "../variables";
import User from "../models/user";
import { sendMail } from "../helpers";
import TelemetryService from "./TelemetryService";
type SecretMatch = {
Description: string;
@@ -30,19 +30,10 @@ type SecretMatch = {
Tags: string[];
RuleID: string;
Fingerprint: string;
FingerPrintWithoutCommitId: string
};
export = async (app: Probot) => {
// connect to DB
initDatabase()
setTransporter(await initSmtp());
app.on("installation.created", async (context) => {
const { payload } = context;
// console.log("payload==>", payload.installation.repository_selection)
})
export default async (app: Probot) => {
app.on("installation.deleted", async (context) => {
const { payload } = context;
const { installation, repositories } = payload;
@@ -50,8 +41,10 @@ export = async (app: Probot) => {
await GitRisks.deleteMany({ installationId: installation.id })
await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id })
} else {
for (const repository of repositories) {
await GitRisks.deleteMany({ repositoryId: repository.id })
if (repositories) {
for (const repository of repositories) {
await GitRisks.deleteMany({ repositoryId: repository.id })
}
}
}
})
@@ -59,9 +52,13 @@ export = async (app: Probot) => {
app.on("push", async (context) => {
const { payload } = context;
const { commits, repository, installation, pusher } = payload;
const [owner, repo] = repository.full_name.split('/');
const [owner, repo] = repository.full_name.split("/");
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation.id }).lean()
if (!commits || !repository || !installation || !pusher) {
return
}
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation?.id }).lean()
if (!installationLinkToOrgExists) {
return
}
@@ -78,31 +75,30 @@ export = async (app: Probot) => {
});
const data: any = fileContentsResponse.data;
const fileContent = Buffer.from(data.content, 'base64').toString();
const fileContent = Buffer.from(data.content, "base64").toString();
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // to count lines correctly
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // extra line to count lines correctly
for (const finding of findings) {
const fingerPrint = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
finding.Fingerprint = fingerPrint
const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`
finding.Fingerprint = fingerPrintWithCommitId
finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId
finding.Commit = commit.id
finding.File = filepath
finding.Author = commit.author.name
finding.Email = commit.author.email
allFindingsByFingerprint[fingerPrint] = finding
finding.Email = commit?.author?.email ? commit?.author?.email : ""
allFindingsByFingerprint[fingerPrintWithCommitId] = finding
}
} catch (error) {
console.error(`Error fetching content for ${filepath}`, error);
console.error(`Error fetching content for ${filepath}`, error); // eslint-disable-line
}
}
}
// change to update
const noneFalsePositiveFindings = {}
for (const key in allFindingsByFingerprint) {
const risk = await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint },
{
@@ -114,11 +110,6 @@ export = async (app: Probot) => {
}, {
upsert: true
}).lean()
if (risk.status != STATUS_RESOLVED_FALSE_POSITIVE) {
noneFalsePositiveFindings[key] = { ...convertKeysToLowercase(allFindingsByFingerprint[key]) }
}
}
// get emails of admins
const adminsOfWork = await MembershipOrg.find({
@@ -137,39 +128,30 @@ export = async (app: Probot) => {
const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
if (Object.keys(allFindingsByFingerprint).length) {
await sendMail({
template: "secretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
recipients: usersToNotify,
substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email,
pusher_name: pusher.name
}
});
}
// TODO
// don't notify if the risk is marked as false positive
await sendMail({
template: "secretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
recipients: [pusher.email, ...adminOrOwnerEmails],
substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email,
pusher_name: pusher.name
}
});
});
app.on(['pull_request.opened', 'pull_request.synchronize'], async (context) => {
const { payload } = context;
const { pull_request } = payload
if (false) {
const check = {
owner: pull_request.head.repo.owner.login,
repo: pull_request.head.repo.name,
name: 'Secret Detection',
head_sha: pull_request.head.sha,
status: 'completed',
conclusion: 'failure',
output: {
title: `X Secrets detected`,
summary: 'We detected potential leaked secret(s) in your pull request.',
},
};
return context.octokit.checks.create(check);
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "cloud secret scan",
distinctId: pusher.email,
properties: {
numberOfCommitsScanned: commits.length,
numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
}
});
}
});
};
@@ -254,28 +236,13 @@ function deleteTempFolder(folderPath: string): Promise<void> {
});
}
const initDatabase = async () => {
try {
await mongoose.connect(process.env.MONGO_URL);
// allow empty strings to pass the required validator
mongoose.Schema.Types.String.checkRequired(v => typeof v === "string");
console.log("Database connection established");
} catch (err) {
console.log(`Unable to establish Database connection due to the error.\n${err}`);
}
return mongoose.connection;
}
function convertKeysToLowercase<T>(obj: T): T {
const convertedObj = {} as T;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1);
convertedObj[lowercaseKey] = obj[key];
convertedObj[lowercaseKey as keyof T] = obj[key];
}
}
@@ -0,0 +1,87 @@
import { Types } from "mongoose";
import Folder from "../models/folder";
import Secret, { ISecret } from "../models/secret";
import SecretImport from "../models/secretImports";
import { getFolderByPath } from "./FolderService";
type TSecretImportFid = { environment: string; folderId: string; secretPath: string };
export const getAllImportedSecrets = async (
workspaceId: string,
environment: string,
folderId = "root"
) => {
const secImports = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!secImports) return [];
if (secImports.imports.length === 0) return [];
const importedEnv: Record<string, boolean> = {}; // to get folders from all environment
secImports.imports.forEach((el) => (importedEnv[el.environment] = true));
const folders = await Folder.find({
workspace: workspaceId,
environment: { $in: Object.keys(importedEnv) }
});
const importedSecByFid: TSecretImportFid[] = [];
secImports.imports.forEach((el) => {
const folder = folders.find((fl) => fl.environment === el.environment);
if (folder) {
const secPathFolder = getFolderByPath(folder.nodes, el.secretPath);
if (secPathFolder)
importedSecByFid.push({
environment: el.environment,
folderId: secPathFolder.id,
secretPath: el.secretPath
});
} else {
if (el.secretPath === "/") {
// this happens when importing with a fresh env without any folders
importedSecByFid.push({ environment: el.environment, folderId: "root", secretPath: "/" });
}
}
});
if (importedSecByFid.length === 0) return [];
const secsGroupedByRef = await Secret.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId),
type: "shared"
}
},
{
$group: {
_id: {
environment: "$environment",
folderId: "$folder"
},
secrets: { $push: "$$ROOT" }
}
},
{
$match: {
$or: importedSecByFid.map(({ environment, folderId: fid }) => ({
"_id.environment": environment,
"_id.folderId": fid
}))
}
}
]);
// now let stitch together secrets.
const importedSecrets: Array<TSecretImportFid & { secrets: ISecret[] }> = [];
importedSecByFid.forEach(({ environment, folderId, secretPath }) => {
const secretsGrouped = secsGroupedByRef.find(
(el) => el._id.environment === environment && el._id.folderId === folderId
);
if (secretsGrouped) {
importedSecrets.push({ secretPath, folderId, environment, secrets: secretsGrouped.secrets });
}
});
return importedSecrets;
};
+11 -9
View File
@@ -7,14 +7,16 @@ import EventService from "./EventService";
import IntegrationService from "./IntegrationService";
import TokenService from "./TokenService";
import SecretService from "./SecretService";
import GithubSecretScanningService from "./GithubSecretScanningService"
export {
TelemetryService,
DatabaseService,
BotService,
BotOrgService,
EventService,
IntegrationService,
TokenService,
SecretService,
}
TelemetryService,
DatabaseService,
BotService,
BotOrgService,
EventService,
IntegrationService,
TokenService,
SecretService,
GithubSecretScanningService
}
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Incident alert: secret leaked</title>
</head>
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
by {{pusher_name}} ({{pusher_email}}). If
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
in the given programming. This will prevent future notifications from being sent out for those secret(s).</p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your <a
href="https://app.infisical.com/">Infisical
dashboard</a>.</p>
</body>
</html>
+14 -2
View File
@@ -26,6 +26,7 @@ export const INTEGRATION_SUPABASE = "supabase";
export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_CODEFRESH = "codefresh";
export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@@ -41,7 +42,8 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH
]);
// integration types
@@ -71,6 +73,7 @@ export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com";
export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com";
export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com";
export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api";
export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [
@@ -245,7 +248,16 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
docsLink: ""
}
},
{
name: "Codefresh",
slug: "codefresh",
image: "Codefresh.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
]
return INTEGRATION_OPTIONS;
+4
View File
@@ -235,6 +235,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId)
if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true")
}
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
+42 -31
View File
@@ -272,40 +272,51 @@ type GetNewAccessTokenWithRefreshTokenResponse struct {
}
type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
}
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 GetEncryptedSecretsV3Response struct {
Secrets []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"`
} `json:"secrets"`
Secrets []EncryptedSecretV3 `json:"secrets"`
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
}
type CreateSecretV3Request struct {
+7 -1
View File
@@ -87,7 +87,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
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")
@@ -186,6 +191,7 @@ func init() {
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
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("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
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 ")
+7 -1
View File
@@ -54,12 +54,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err)
}
includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err)
}
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
if err != nil {
util.HandleError(err)
}
@@ -647,6 +652,7 @@ func init() {
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
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")
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd)
+1
View File
@@ -65,4 +65,5 @@ type GetAllSecretsParameters struct {
TagSlugs string
WorkspaceId string
SecretsPath string
IncludeImport bool
}
+58 -14
View File
@@ -17,7 +17,7 @@ import (
"github.com/rs/zerolog/log"
)
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -45,9 +45,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: environment,
SecretPath: secretPath,
WorkspaceId: serviceTokenDetails.Workspace,
Environment: environment,
SecretPath: secretPath,
IncludeImport: includeImports,
})
if err != nil {
@@ -64,15 +65,22 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt the required workspace key")
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
if includeImports {
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, err
}
}
return plainTextSecrets, serviceTokenDetails, nil
}
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New()
httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
@@ -114,8 +122,9 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId,
Environment: environmentName,
WorkspaceId: workspaceId,
Environment: environmentName,
IncludeImport: includeImports,
// TagSlugs: tagSlugs,
}
@@ -124,19 +133,53 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil {
return nil, err
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
if includeImports {
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
if err != nil {
return nil, err
}
}
return plainTextSecrets, nil
}
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]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, err := GetPlainTextSecrets(plainTextWorkspaceKey, importSec.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to decrypt your imported secrets [err=%v]", err)
}
for _, sec := range plainTextImportedSecrets {
if _, ok := hasOverriden[sec.Key]; !ok {
secrets = append(secrets, sec)
hasOverriden[sec.Key] = true
}
}
}
return secrets, nil
}
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
var infisicalToken string
if params.InfisicalToken == "" {
@@ -179,7 +222,8 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
}
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath)
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId,
params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -199,7 +243,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
} else {
log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath)
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath, params.IncludeImport)
}
return secretsToReturn, errorToReturn
@@ -427,9 +471,9 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn
}
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) {
func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) ([]models.SingleEnvironmentVariable, error) {
plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets.Secrets {
for _, secret := range encryptedSecrets {
// Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil {
-25
View File
@@ -58,31 +58,6 @@ services:
networks:
- infisical-dev
# git-app:
# container_name: infisical-dev-git-app
# restart: unless-stopped
# depends_on:
# - mongo
# - smtp-server
# - backend
# - frontend
# volumes:
# - ./secret-engine/src:/app/src/ # mounted whole src to avoid missing reload on new files
# ports:
# - "3000:3001"
# build:
# context: ./secret-engine
# dockerfile: Dockerfile.dev
# command: npm run start
# env_file: ./secret-engine/.env
# environment:
# - NODE_ENV=development
# - MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# networks:
# - infisical-dev
# extra_hosts:
# - "host.docker.internal:host-gateway"
mongo:
image: mongo
container_name: infisical-dev-mongo
+1 -1
View File
@@ -6,7 +6,7 @@ The changelog below reflects new product developments and updates on a monthly b
## July 2023
- Released [secret referencing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments.
- Released [secret referencing and importing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments.
- Added the [intergation with Laravel Forge](https://infisical.com/docs/integrations/cloud/laravel-forge).
- Redesigned the project/organization experience.
+18 -5
View File
@@ -3,7 +3,8 @@ title: 'Install'
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
---
The Infisical CLI can be used to access secrets across various environments, whether it's local development, CI/CD, staging, or production.
The Infisical CLI is 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
@@ -57,7 +58,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash
apk update && sudo apk add infisical
```
###
<Tip>
If you are installing the CLI in production environments, we highly recommend to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
</Tab>
<Tab title="RedHat/CentOs/Amazon">
Add Infisical repository
@@ -71,7 +75,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash
sudo yum install infisical
```
###
<Tip>
If you are installing the CLI in production environments, we highly recommend to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
</Tab>
<Tab title="Debian/Ubuntu">
Add Infisical repository
@@ -86,7 +93,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash
sudo apt-get update && sudo apt-get install -y infisical
```
###
<Tip>
If you are installing the CLI in production environments, we highly recommend to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
</Tab>
<Tab title="Arch Linux">
Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin)
@@ -95,6 +105,9 @@ The Infisical CLI can be used to access secrets across various environments, whe
yay -S infisical-bin
```
###
<Tip>
If you are installing the CLI in production environments, we highly recommend to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
</Tab>
</Tabs>
+1 -1
View File
@@ -8,7 +8,7 @@ The distinguishing factor, however, is the authentication method used.
<Tabs>
<Tab title="Local development">
To use the Infisical CLI in your development environment, simply run the following command and follow the interactive guide.
To use the Infisical CLI in your development environment, simply run the command below and follow the interactive guide.
```bash
infisical login
@@ -17,7 +17,7 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
Store secrets like API keys, database credentials, environment variables with Infisical
</Card>
## Integrate with Infisical
## Access secrets
<CardGroup cols={2}>
<Card href="../../cli/overview" title="Command Line Interface (CLI)" icon="square-terminal" color="#3775a9">
@@ -31,11 +31,11 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
>
Fetch secrets with any programming language on demand
</Card>
<Card href="/documentation/getting-started/docker" title="Docker" icon="docker" color="#0078d3">
<Card href="../../integrations/platforms/docker-intro" title="Docker" icon="docker" color="#0078d3">
Inject secrets into Docker containers
</Card>
<Card
href="/documentation/getting-started/kubernetes"
href="../../integrations/platforms/kubernetes"
title="Kubernetes"
icon="server"
color="#3775a9"
@@ -1,5 +1,5 @@
---
title: "Reference Secrets"
title: "Reference/Import Secrets"
description: "How to use reference secrets in Infisical"
---
@@ -29,9 +29,24 @@ Here are a few more examples to help you understand how to reference secrets in
## Fetching fully constructed values
Secret referencing combines multiple secrets into one unified value, reconstructed only on the client side. To retrieve this value, you need access to read the environment and [folder](./folder) from where the secrets originate.
For instance, to access a secret 'A' composed of secrets 'B' and 'C' from different environments, you must have read access to both.
For instance, to access a secret 'A' composed of secrets 'B' and 'C' from different environments, you must have read access to both 'A' and 'B'
When using [service tokens](./token) to fetch referenced secrets, ensure the service token has read access to all referenced environments and folders.
Without proper permissions, the final secret value may be incomplete.
## Import entire folders
While secret referencing effectively minimizes duplication, there might be instances where you need to import or replicate an entire folder's secrets into another. This can be achieved using the 'Import' feature.
This feature allows you to link secrets from one environment/folder into another environment/folder. It proves beneficial when you have common secrets that need to be available across multiple environments/folders.
To add an import, simply click on the `Add import` button and provide the environment and secret path from where the secrets should be imported.
![secret import change order](../../images/secret-import-add.png)
The hierarchy of importing secrets is governed by a "last-one-wins" rule. This means the sequence in which you import matters - the final folder imported will override secrets from any prior folders.
Moreover, any secrets you define directly in your environment will take precedence over secrets from any imported folders.
You can modify this sequence by dragging and rearranging the folders using the `Change Order` drag handle.
![secret import change order](../../images/secret-import-change-order.png)
+1 -1
View File
@@ -12,7 +12,7 @@ This level of control not only ensures maximum flexibility but also significantl
## Creating a service token
To generate the the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access.
To generate the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access.
![token add](../../images/project-token-add.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

@@ -11,11 +11,11 @@ Prerequisites:
Follow this [guide](./docker) to configure the Infisical CLI for each service that you wish to inject environment variables into; you'll have to update the Dockerfile of each service.
## Generate Infisical Tokens
## Generate service token
Generate a unique [Infisical Token](/documentation/platform/token) for each service.
## Add Infisical Tokens to your Docker Compose file
## Feed service token to your Docker Compose file
For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a unique identifier variable.
@@ -0,0 +1,25 @@
---
title: "Docker"
description: "Learn how to feed secrets from Infisical into your docker application"
---
There are many methods to inject Infisical secrets to docker-based applications.
Regardless of which method you choose, these methods will inject secrets from Infisical as environment variables into your Docker container.
<Card title="Docker Entrypoint" color="#ea5a0c" href="./docker">
Install and run your app start command with Infisical CLI
</Card>
<CardGroup cols={2}>
<Card title="Docker run" color="#0285c7" href="./docker-pass-envs">
Feed secrets via `--env-file` flag in docker run command
</Card>
<Card title="Docker compose" color="#16a34a" href="./docker-compose">
Inject secrets to multiple services using Docker Compose
</Card>
</CardGroup>
<Info>
The main difference between the "Docker Entrypoint" and "Docker run" approach is where the Infisical CLI is installed.
In most production settings, it's typically inconvenient to have the Infisical CLI installed and executed externally.
As a result, we suggest using the "Docker Entrypoint" method for production purposes.
However, if this limitation doesn't apply to you, select the method that best fits your needs.
</Info>
@@ -0,0 +1,48 @@
---
title: "Docker Run"
description: "Pass secrets to your docker container at run time"
---
This method allows you to feed secrets from Infisical into your container using the `--env-file` flag of `docker run` command.
Rather than giving the flag a file path to your env file, you'll use the Infisical CLI to create a virtual file path.
For this method to function as expected, you must have a bash shell (for processing substitution) and the [Infisical CLI](../../cli/overview) installed in the environment where you will be running the `docker run` command.
## 1. Authentication
If you are already logged in via the CLI you can skip this step. Otherwise, head to your project settings in Infisical Cloud to generate an [Infisical Token](/documentation/platform/token). The service token will allow you to authenticate and fetch secrets from Infisical.
Once you have created a service token with the required permissions, you'll need to feed the token to the CLI.
#### Pass as flag
You may use the --token flag to set the token
```bash
infisical export --token=<>
```
#### Pass via shell environment variable
The CLI is configured to look for an environment variable named `INFISICAL_TOKEN`. If set, it'll attempt to use it for authentication.
```bash
export INFISICAL_TOKEN=<>
```
<Warning>
In production scenarios, please to avoid using the `infisical login` command and instead use a [service token](/documentation/platform/token).
</Warning>
## 2. Run your docker command with Infisical
Next, use the --env-file flag of the `docker run` command with Infisical CLI to point to your secrets.
Under the hood, this command will fetch secrets from Infisical and serve them as a file to the `--env-file` flag.
```bash
# In this example, executing a docker run command will initiate an empty Alpine container and display the environment variables passed to it by Infisical.
docker run --rm --env-file <(infisical export --format=dotenv) alpine printenv
```
To view all options of the `export` command, click [here](../../cli/commands/export)
<Warning>
When using the --env-file option, Docker does not have the capability to support secrets that span multiple lines.
</Warning>
+15 -13
View File
@@ -1,11 +1,10 @@
---
title: "Docker"
title: "Docker Entrypoint"
description: "How to use Infisical to inject environment variables into a Docker container."
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
This approach allows you to inject secrets from Infisical directly into your application.
This is achieved by installing the Infisical CLI into your docker image and modifying your start command to execute with Infisical.
## Add the Infisical CLI to your Dockerfile
@@ -33,6 +32,10 @@ Prerequisites:
```
</Tab>
</Tabs>
####
<Tip>
We recommend you to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
## Modify the start command in your Dockerfile
@@ -48,18 +51,17 @@ CMD ["infisical", "run", "--", "npm", "run", "start"]
CMD ["infisical", "run", "--command", "npm run start && ..."]
```
## Generate an Infisical Token
## Generate an service token
Head to your project settings in Infisical Cloud to generate an [Infisical Token](/documentation/platform/token).
Head to your project settings in the Infisical dashboard to generate an [service token](/documentation/platform/token).
This service token will allow you to authenticate and fetch secrets from Infisical.
Once you have created a service token with the required permissions, you’ll need to feed the token to the CLI installed in your docker container.
## Feed Docker your Infisical Token
## Feed service token to docker container
The last step is to give the Infisical CLI installed in your Docker container access to the service token. This will allow the CLI to fetch and inject the secrets into your application.
To feed the service token to the container, use the INFISICAL_TOKEN environment variable as shown below.
```bash
docker run --env INFISICAL_TOKEN=[token] [DOCKER-IMAGE]...
```
<Info>
The Infisical CLI uses the detected `INFISICAL_TOKEN` environment variable to authenticate, retrieve, and inject the environment variables which the token is authorized for.
</Info>
+4 -2
View File
@@ -91,8 +91,8 @@
"documentation/getting-started/introduction",
"documentation/getting-started/platform",
"documentation/getting-started/sdks",
"documentation/getting-started/docker",
"documentation/getting-started/kubernetes",
"integrations/platforms/kubernetes",
"integrations/platforms/docker-intro",
"documentation/getting-started/api"
]
},
@@ -187,7 +187,9 @@
{
"group": "Docker",
"pages": [
"integrations/platforms/docker-intro",
"integrations/platforms/docker",
"integrations/platforms/docker-pass-envs",
"integrations/platforms/docker-compose"
]
},
+1 -1
View File
@@ -8,7 +8,7 @@ Self-hosted Infisical allows you to maintain your sensitive information within y
Choose from a variety of deployment options listed below to get started.
<Card
title="Kubernetes"
title="Kubernetes (recommended)"
color="#ea5a0c"
href="deployment-options/kubernetes-helm"
>
+109
View File
@@ -5,6 +5,9 @@
"packages": {
"": {
"dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15",
@@ -2468,6 +2471,68 @@
"node": ">=10.0.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.6",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.7",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@@ -24491,6 +24556,50 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"dev": true
},
"@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"requires": {
"tslib": "^2.0.0"
}
},
"@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"requires": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
}
},
"@dnd-kit/modifiers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
"requires": {
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
}
},
"@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"requires": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
}
},
"@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"requires": {
"tslib": "^2.0.0"
}
},
"@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
+3
View File
@@ -13,6 +13,9 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15",
+2 -1
View File
@@ -20,7 +20,8 @@ const integrationSlugNameMapping: Mapping = {
'supabase': 'Supabase',
'checkly': 'Checkly',
'hashicorp-vault': 'Vault',
'cloudflare-pages': 'Cloudflare Pages'
'cloudflare-pages': 'Cloudflare Pages',
'codefresh': 'Codefresh'
}
const envMapping: Mapping = {
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@ export const EmptyState = ({
className
)}
>
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
<FontAwesomeIcon icon={icon} size={iconSize} />
<div className="flex flex-row items-center py-4">
<div className="text-sm text-bunker-300">{title}</div>
<div>{children}</div>
+1
View File
@@ -7,6 +7,7 @@ export * from "./keys";
export * from "./organization";
export * from "./ssoConfig";
export * from "./secretFolders";
export * from "./secretImports";
export * from "./secrets";
export * from "./secretSnapshots";
export * from "./serviceAccounts";
@@ -5,5 +5,5 @@ export {
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches
useGetIntegrationAuthVercelBranches,
} from "./queries";
@@ -19,6 +19,7 @@ const integrationAuthKeys = {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
@@ -64,6 +65,7 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
return data.teams;
};
const fetchIntegrationAuthVercelBranches = async ({
integrationAuthId,
appId
@@ -224,6 +226,7 @@ export const useGetIntegrationAuthRailwayServices = ({
});
};
export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient();
@@ -235,3 +238,4 @@ export const useDeleteIntegrationAuth = () => {
}
});
};
@@ -0,0 +1,2 @@
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
export { useGetImportedSecrets, useGetSecretImports } from "./queries";
@@ -0,0 +1,78 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { secretImportKeys } from "./queries";
import { TCreateSecretImportDTO, TDeleteSecretImportDTO, TUpdateSecretImportDTO } from "./types";
export const useCreateSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretImportDTO>({
mutationFn: async ({ secretImport, environment, workspaceId, folderId }) => {
const { data } = await apiRequest.post("/api/v1/secret-imports", {
secretImport,
environment,
workspaceId,
folderId
});
return data;
},
onSuccess: (_, { workspaceId, environment, folderId }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
);
}
});
};
export const useUpdateSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretImportDTO>({
mutationFn: async ({ environment, workspaceId, folderId, secretImports, id }) => {
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
secretImports,
environment,
workspaceId,
folderId
});
return data;
},
onSuccess: (_, { workspaceId, environment, folderId }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
);
}
});
};
export const useDeleteSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretImportDTO>({
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
data: {
secretImportPath,
secretImportEnv
}
});
return data;
},
onSuccess: (_, { workspaceId, environment, folderId }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
);
}
});
};
@@ -0,0 +1,124 @@
import { useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { TGetImportedSecrets, TImportedSecrets, TSecretImports } from "./types";
export const secretImportKeys = {
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-imports"
],
getSecretImportSecrets: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-import-sec"
]
};
const fetchSecretImport = async (workspaceId: string, environment: string, folderId?: string) => {
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
"/api/v1/secret-imports",
{
params: {
workspaceId,
environment,
folderId
}
}
);
return data.secretImport;
};
export const useGetSecretImports = (workspaceId: string, env: string, folderId?: string) =>
useQuery({
enabled: Boolean(workspaceId) && Boolean(env),
queryKey: secretImportKeys.getProjectSecretImports(workspaceId, env, folderId),
queryFn: () => fetchSecretImport(workspaceId, env, folderId)
});
const fetchImportedSecrets = async (
workspaceId: string,
environment: string,
folderId?: string
) => {
const { data } = await apiRequest.get<{ secrets: TImportedSecrets }>(
"/api/v1/secret-imports/secrets",
{
params: {
workspaceId,
environment,
folderId
}
}
);
return data.secrets;
};
export const useGetImportedSecrets = ({
workspaceId,
environment,
folderId,
decryptFileKey
}: TGetImportedSecrets) =>
useQuery({
enabled: Boolean(workspaceId) && Boolean(environment) && Boolean(decryptFileKey),
queryKey: secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId),
queryFn: () => fetchImportedSecrets(workspaceId, environment, folderId),
select: useCallback(
(data: TImportedSecrets) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data.map((el) => ({
environment: el.environment,
secretPath: el.secretPath,
folderId: el.folderId,
secrets: el.secrets.map((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
return {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
})
}));
},
[decryptFileKey]
)
});
@@ -0,0 +1,56 @@
import { EncryptedSecret } from "../secrets/types";
import { UserWsKeyPair } from "../types";
export type TSecretImports = {
_id: string;
workspaceId: string;
environment: string;
folderId: string;
imports: Array<{ environment: string; secretPath: string }>;
createdAt: string;
updatedAt: string;
};
export type TImportedSecrets = {
environment: string;
secretPath: string;
folderId: string;
secrets: EncryptedSecret[];
}[];
export type TGetImportedSecrets = {
workspaceId: string;
environment: string;
folderId?: string;
decryptFileKey: UserWsKeyPair;
};
export type TCreateSecretImportDTO = {
workspaceId: string;
environment: string;
folderId?: string;
secretImport: {
environment: string;
secretPath: string;
};
};
export type TUpdateSecretImportDTO = {
id: string;
workspaceId: string;
environment: string;
folderId?: string;
secretImports: Array<{
environment: string;
secretPath: string;
}>;
};
export type TDeleteSecretImportDTO = {
id: string;
workspaceId: string;
environment: string;
folderId?: string;
secretImportPath: string;
secretImportEnv: string;
};
@@ -24,6 +24,10 @@ export const secretKeys = {
{ workspaceId, env, folderId },
"secrets"
],
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-imports"
],
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
};
+2
View File
@@ -34,6 +34,7 @@ export type DecryptedSecret = {
valueOverride?: string;
idOverride?: string;
overrideAction?: string;
folderId?: string;
};
export type EncryptedSecretVersion = {
@@ -98,6 +99,7 @@ export type GetProjectSecretsDTO = {
folderId?: string;
secretPath?: string;
isPaused?: boolean;
include_imports?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void;
};
+13
View File
@@ -16,6 +16,7 @@ import {
CreateAPIKeyRes,
DeletOrgMembershipDTO,
OrgUser,
RenameUserDTO,
TokenVersion,
UpdateOrgUserRoleDTO,
User} from "./types";
@@ -45,6 +46,18 @@ const fetchUserAction = async (action: string) => {
return data.userAction;
};
export const useRenameUser = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, RenameUserDTO>({
mutationFn: ({ newName }) =>
apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }),
onSuccess: () => {
queryClient.invalidateQueries(userKeys.getUser);
}
});
};
export const useGetUserAction = (action: string) =>
useQuery({
queryKey: userKeys.userAction,
+4
View File
@@ -67,6 +67,10 @@ export type CreateAPIKeyRes = {
apiKeyData: APIKeyData;
}
export type RenameUserDTO = {
newName: string;
};
export type APIKeyData = {
_id: string;
name: string;
+10 -1
View File
@@ -408,7 +408,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
<a>
<MenuItem
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
icon="system-outline-168-view-headline"
>
Audit Logs
@@ -473,6 +472,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</a>
</Link>
<Link href={`/org/${currentOrg?._id}/secret-scanning`} passHref>
<a>
<MenuItem
isSelected={router.asPath === `/org/${currentOrg?._id}/secret-scanning`}
icon="system-outline-69-document-scan"
>
Secret Scanning
</MenuItem>
</a>
</Link>
<Link href={`/org/${currentOrg?._id}/billing`} passHref>
<a>
<MenuItem
@@ -1,7 +1,7 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
export type GitRisks = {
id: string;
_id: string;
description: string;
startLine: string;
endLine: string;
@@ -20,6 +20,7 @@ export type GitRisks = {
tags: string[];
ruleID: string;
fingerprint: string;
status: string;
isFalsePositive: boolean; // New field for marking risks as false positives
isResolved: boolean; // New field for marking risks as resolved
@@ -16,7 +16,7 @@ const linkGitAppInstallationWithOrganization = (installationId: string, sessionI
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
return true
}
console.log("Failed to link installation to organization");
return undefined;
@@ -0,0 +1,64 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function CodefreshCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "codefresh",
accessId: null,
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/codefresh/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Codefresh Integration</CardTitle>
<FormControl
label="Codefresh API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Codefresh
</Button>
</Card>
</div>
);
}
CodefreshCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,155 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function CodefreshCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Codefresh Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Codefresh Service" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No services found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
CodefreshCreateIntegrationPage.requireAuth = true;
@@ -11,6 +11,7 @@ export default function SettingsBilling() {
<Head>
<title>{t("common.head-title", { title: t("billing.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<BillingSettingsPage />
</div>
@@ -0,0 +1,89 @@
import { useEffect, useState } from "react";
import Head from "next/head";
import { useRouter } from "next/router"
import { Button } from "@app/components/v2";
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
export default function SecretScanning() {
const router = useRouter()
const queryParams = router.query
const [integrationEnabled, setIntegrationStatus] = useState(false)
useEffect(()=>{
const linkInstallation = async () => {
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
try {
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
if (isLinked){
router.reload()
}
console.log("installation verification complete")
}catch (e){
console.log("app installation is stale, start new session", e)
}
}
}
const fetchInstallationStatus = async () => {
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
setIntegrationStatus(status)
}
fetchInstallationStatus()
linkInstallation()
},[queryParams.state, queryParams.installation_id])
const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
}
return (
<div>
<Head>
<title>Secret scanning</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div>
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
<div className="flex flex-col items-start">
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div>
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div>
</div>
{integrationEnabled ? (
<div>
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/>
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/>
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/>
</div>
) : (
<div className="flex items-center h-[3.25rem]">
<Button
variant="solid"
colorSchema="primary"
onClick={generateNewIntegrationSession}
className="py-2 h-min"
>
Integrate with GitHub
</Button>
</div>
)}
</div>
<SecretScanningLogsTable />
</div>
</div>
</div>
);
}
SecretScanning.requireAuth = true;
+15
View File
@@ -0,0 +1,15 @@
import { useEffect } from "react";
import { useRouter } from "next/router"
export default function SecretScanning() {
const router = useRouter();
useEffect(()=>{
router.push(`${router.asPath.split("secret-scanning")[0]}/org/${localStorage.getItem("orgData.id")}/secret-scanning${router.asPath.split("secret-scanning")[1]}`)
}, [])
return <div/>;
}
SecretScanning.requireAuth = true;
@@ -1,156 +0,0 @@
import { useEffect, useState } from "react";
import Head from "next/head";
import { useRouter } from "next/router"
import createNewIntegrationSession from "../api/secret-scanning/createSecretScanningSession";
import getInstallationStatus from "../api/secret-scanning/getInstallationStatus";
import getRisksByOrganization, { GitRisks } from "../api/secret-scanning/getRisksByOrganization";
import linkGitAppInstallationWithOrganization from "../api/secret-scanning/linkGitAppInstallationWithOrganization";
import { RiskStatus } from "../api/secret-scanning/updateRiskStatus";
export default function SecretScanning() {
const router = useRouter()
const { state, installationId} = router.query
const [integrationEnabled, setIntegrationStatus] = useState(false)
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
const [selectedRiskStatus, setSelectedRiskStatus] = useState("");
const handleSelectRiskStatusUpdate = (event: any) => {
setSelectedRiskStatus(event.target.value);
};
console.log("selectedRiskStatus===>", selectedRiskStatus)
useEffect(()=>{
const fetchRisks = async () =>{
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
setGitRisks(risks)
}
const linkInstallation = async () => {
if (typeof state === "string" && typeof installationId === "string"){
try {
await linkGitAppInstallationWithOrganization(installationId as string, state as string)
console.log("installation verification complete")
}catch (e){
console.log("app installation is stale, start new session", e)
}
}
}
const fetchInstallationStatus = async () => {
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
setIntegrationStatus(status)
}
fetchInstallationStatus()
linkInstallation()
fetchRisks()
},[state, installationId])
const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
}
return (
<div>
<Head>
<title>Secret scanning</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
{/* <NavHeader pageName={"Secret scanning"} isProjectRelated={false} /> */}
<div className="text-left">
{integrationEnabled ? (
<b className="text-green-500">Git app is linked to this organization</b>
) : (
<button
type="button"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={generateNewIntegrationSession}
>
Integrate with GitHub
</button>
)}
</div>
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Secret Type
</th>
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
View Risk
</th>
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Info
</th>
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Action
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{gitRisks.map((risk) => {
return (
<tr key={risk.ruleID}>
<td className="py-4 px-6 whitespace-nowrap">{risk.createdAt}</td>
<td className="py-4 px-6 whitespace-nowrap">{risk.ruleID}</td>
<td className="py-4 px-6 whitespace-nowrap">
<a
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
target="_blank"
className="text-red-500" rel="noreferrer"
>
View Exposed Secret
</a>
</td>
<td className="py-4 px-6 whitespace-nowrap">
<div className="font-bold">
<a href={`https://github.com/${risk.repositoryFullName}`}>
{risk.repositoryFullName}
</a>
</div>
<div className="text-xs">
<span>{risk.file}</span><br/>
<br/>
<span className="font-bold">{risk.author}</span><br/>
<span>{risk.email}</span>
</div>
</td>
<td className="py-4 px-6 whitespace-nowrap">
{risk.isResolved ? "Resolved" : "Needs Attention"}
</td>
<td className="py-4 px-6 whitespace-nowrap">
{risk.isResolved ? "Resolved" : "Needs Attention"}
</td>
<td className="py-4 px-6 whitespace-nowrap">
<select
value={selectedRiskStatus}
onChange={handleSelectRiskStatusUpdate}
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option>Unresolved</option>
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive</option>
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolve risk</option>
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolve</option>
</select>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
+287 -112
View File
@@ -3,6 +3,19 @@ import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import {
closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors
} from "@dnd-kit/core";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { arrayMove } from "@dnd-kit/sortable";
import {
faAngleDown,
faArrowLeft,
faCheck,
faClockRotateLeft,
@@ -10,12 +23,14 @@ import {
faDownload,
faEye,
faEyeSlash,
faFileImport,
faFolderPlus,
faMagnifyingGlass,
faPlus
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { useQueryClient } from "@tanstack/react-query";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -41,10 +56,14 @@ import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
import {
useBatchSecretsOp,
useCreateFolder,
useCreateSecretImport,
useCreateWsTag,
useDeleteFolder,
useDeleteSecretImport,
useGetImportedSecrets,
useGetProjectFolders,
useGetProjectSecrets,
useGetSecretImports,
useGetSecretVersion,
useGetSnapshotSecrets,
useGetUserAction,
@@ -55,7 +74,8 @@ import {
useGetWsTags,
usePerformSecretRollback,
useRegisterUserAction,
useUpdateFolder
useUpdateFolder,
useUpdateSecretImport
} from "@app/hooks/api";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { WorkspaceEnv } from "@app/hooks/api/types";
@@ -71,6 +91,8 @@ import {
import { PitDrawer } from "./components/PitDrawer";
import { SecretDetailDrawer } from "./components/SecretDetailDrawer";
import { SecretDropzone } from "./components/SecretDropzone";
import { SecretImportForm } from "./components/SecretImportForm";
import { SecretImportSection } from "./components/SecretImportSection";
import { SecretInputRow } from "./components/SecretInputRow";
import { SecretTableHeader } from "./components/SecretTableHeader";
import {
@@ -84,7 +106,7 @@ import {
} from "./DashboardPage.utils";
const USER_ACTION_PUSH = "first_time_secrets_pushed";
type TDeleteSecretImport = { environment: string; secretPath: string };
/*
* Some imp aspects to consider. Here there are multiple stats changing
* Thus ideally we need to use a context. But instead we rely on react hook form
@@ -113,7 +135,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
"compareSecrets",
"folderForm",
"deleteFolder",
"upgradePlan"
"upgradePlan",
"addSecretImport",
"deleteSecretImport"
] as const);
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState("");
@@ -129,6 +153,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { currentWorkspace, isLoading } = useWorkspace();
const { currentOrg } = useOrganization();
const workspaceId = currentWorkspace?._id as string;
const selectedEnvSlug = selectedEnv?.slug || "";
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
useEffect(() => {
@@ -161,7 +187,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env: selectedEnv?.slug || "",
env: selectedEnvSlug,
decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId),
folderId
@@ -169,7 +195,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
workspaceId: workspaceId || "",
environment: selectedEnv?.slug || "",
environment: selectedEnvSlug,
parentFolderId: folderId,
isPaused: isRollbackMode,
sortDir
@@ -182,7 +208,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetchingNextPage
} = useGetWorkspaceSecretSnapshots({
workspaceId,
environment: selectedEnv?.slug || "",
environment: selectedEnvSlug,
folder: folderId,
limit: 10
});
@@ -193,17 +219,18 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetching: isSnapshotChanging
} = useGetSnapshotSecrets({
snapshotId: snapshotId || "",
env: selectedEnv?.slug || "",
env: selectedEnvSlug,
decryptFileKey: latestFileKey!
});
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
workspaceId,
selectedEnv?.slug || "",
selectedEnvSlug,
folderId
);
const { data: wsTags } = useGetWsTags(workspaceId);
// mutation calls
const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
@@ -213,6 +240,50 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { mutateAsync: updateFolder } = useUpdateFolder(folderId);
const { mutateAsync: deleteFolder } = useDeleteFolder(folderId);
const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports(
workspaceId,
selectedEnvSlug,
folderId
);
const { data: importedSecrets } = useGetImportedSecrets({
workspaceId,
decryptFileKey: latestFileKey!,
environment: selectedEnvSlug,
folderId
});
// This is for dnd-kit. As react-query state mutation async
// This will act as a placeholder to avoid a glitching animation on dropping items
const [items, setItems] = useState<
Array<{ environment: string; secretPath: string; id: string }>
>([]);
useEffect(() => {
if (
!isSecretImportCfgFetching ||
// case in which u go to a folder and come back to fill in with cache data
(items.length === 0 && secretImportCfg?.imports?.length !== 0 && isSecretImportCfgFetching)
) {
setItems(
secretImportCfg?.imports?.map((el) => ({
...el,
id: `${el.environment}-${el.secretPath}`
})) || []
);
}
}, [isSecretImportCfgFetching]);
const { mutateAsync: createSecretImport } = useCreateSecretImport();
const { mutate: updateSecretImportSync } = useUpdateSecretImport();
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
);
const method = useForm<FormData>({
// why any: well yup inferred ts expects other keys to defined as undefined
defaultValues: secrets as any,
@@ -319,14 +390,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
await performSecretRollback({
workspaceId,
version: snapshotSecret.version,
environment: selectedEnv?.slug || "",
environment: selectedEnvSlug,
folderId
});
setValue("isSnapshotMode", false);
setSnaphotId(null);
queryClient.invalidateQueries(
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || "")
);
queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug));
createNotification({
text: "Successfully rollback secrets",
type: "success"
@@ -522,6 +591,79 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
}
};
const handleSecretImportCreate = async (env: string, secretPath: string) => {
try {
await createSecretImport({
workspaceId,
environment: selectedEnv?.slug || "",
folderId,
secretImport: {
environment: env,
secretPath
}
});
createNotification({
type: "success",
text: "Successfully create secret link"
});
handlePopUpClose("addSecretImport");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create secret link",
type: "error"
});
}
};
const handleSecretImportDelete = async () => {
const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport
?.data as TDeleteSecretImport;
try {
if (secretImportCfg?._id) {
await deleteSecretImport({
workspaceId,
environment: selectedEnvSlug,
folderId,
id: secretImportCfg?._id,
secretImportEnv: importEnv,
secretImportPath: impSecPath
});
handlePopUpClose("deleteSecretImport");
createNotification({
type: "success",
text: "Successfully removed secret link"
});
}
} catch (err) {
console.error(err);
createNotification({
text: "Failed to remove secret link",
type: "error"
});
}
};
const handleDragEnd = (evt: DragEndEvent) => {
const { active, over } = evt;
if (over?.id && active.id !== over.id) {
const oldIndex = items.findIndex(({ id }) => id === active.id);
const newIndex = items.findIndex(({ id }) => id === over.id);
const newImportOrder = arrayMove(items, oldIndex, newIndex);
setItems(newImportOrder);
updateSecretImportSync({
workspaceId,
environment: selectedEnvSlug,
folderId,
id: secretImportCfg?._id || "",
secretImports: newImportOrder.map((el) => ({
environment: el.environment,
secretPath: el.secretPath
}))
});
}
};
// when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
@@ -534,7 +676,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const isSnapshotSecretEmtpy =
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
const isEmptyPage = isFoldersEmpty && isSecretEmpty;
const isSecretImportEmpty = !secretImportCfg?.imports?.length;
const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty;
if (isSecretsLoading || isEnvListLoading) {
return (
@@ -652,54 +795,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</Button>
</div>
{!isReadOnly && !isRollbackMode && (
<>
<div className="block lg:hidden">
<Tooltip content="Add Folder">
<IconButton
ariaLabel="recovery"
variant="outline_bg"
onClick={() => handlePopUpOpen("folderForm")}
>
<FontAwesomeIcon icon={faFolderPlus} />
</IconButton>
</Tooltip>
</div>
<div className="block lg:hidden">
<Tooltip content="Point-in-time Recovery">
<IconButton
ariaLabel="recovery"
variant="outline_bg"
onClick={() => {
if (secretContainer.current) {
secretContainer.current.scroll({
top: 0,
behavior: "smooth"
});
}
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</Tooltip>
</div>
<div className="hidden lg:block">
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => handlePopUpOpen("folderForm")}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
>
Add Folder
</Button>
</div>
<div className="hidden lg:block">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
<div className="flex flex-row items-center justify-center">
<button
type="button"
onClick={() => {
if (!(isReadOnly || isRollbackMode)) {
if (secretContainer.current) {
secretContainer.current.scroll({
top: 0,
@@ -708,15 +808,48 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
}
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
setSearchFilter("");
}}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
>
Add Secret
</Button>
</div>
</>
}
}}
className="font-semibold bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-l-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 pr-4 duration-200"
>
<FontAwesomeIcon icon={faPlus} className="px-2"/>Add Secret
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
<div className="bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-r-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 duration-200">
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5"/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="mt-1 z-[60] left-20 w-[10.8rem]">
<div className="bg-mineshaft-800 p-1 border border-mineshaft-600 rounded-md">
<div className="w-full pb-1">
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => handlePopUpOpen("folderForm")}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Folder
</Button>
</div>
<div className="w-full">
<Button
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
onClick={() => handlePopUpOpen("addSecretImport")}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Import
</Button>
</div>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{isRollbackMode && (
<Button
@@ -751,49 +884,67 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
ref={secretContainer}
>
{!isEmptyPage && (
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
<table className="secret-table relative">
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto">
<FolderSection
onFolderOpen={handleFolderOpen}
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
folders={folderList}
search={searchFilter}
/>
{fields.map(({ id, _id }, index) => (
<SecretInputRow
key={id}
isReadOnly={isReadOnly}
isRollbackMode={isRollbackMode}
isAddOnly={isAddOnly}
index={index}
searchTerm={searchFilter}
onSecretDelete={onSecretDelete}
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
isSecretValueHidden={isSecretValueHidden}
wsTags={wsTags}
onCreateTagOpen={() => handlePopUpOpen("addTag")}
<DndContext
onDragEnd={handleDragEnd}
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
>
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
<table className="secret-table relative">
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto">
<SecretImportSection
onSecretImportDelete={(impSecEnv, impSecPath) =>
handlePopUpOpen("deleteSecretImport", {
environment: impSecEnv,
secretPath: impSecPath
})
}
secrets={secrets?.secrets}
importedSecrets={importedSecrets}
items={items}
/>
))}
{!isReadOnly && !isRollbackMode && (
<tr>
<td colSpan={3} className="hover:bg-mineshaft-700">
<button
type="button"
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
onClick={onAppendSecret}
>
<FontAwesomeIcon icon={faPlus} />
<span className="ml-2 w-20">Add Secret</span>
</button>
</td>
</tr>
)}
</tbody>
</table>
</TableContainer>
<FolderSection
onFolderOpen={handleFolderOpen}
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
folders={folderList}
search={searchFilter}
/>
{fields.map(({ id, _id }, index) => (
<SecretInputRow
key={id}
isReadOnly={isReadOnly}
isRollbackMode={isRollbackMode}
isAddOnly={isAddOnly}
index={index}
searchTerm={searchFilter}
onSecretDelete={onSecretDelete}
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
isSecretValueHidden={isSecretValueHidden}
wsTags={wsTags}
onCreateTagOpen={() => handlePopUpOpen("addTag")}
/>
))}
{!isReadOnly && !isRollbackMode && (
<tr>
<td colSpan={3} className="hover:bg-mineshaft-700">
<button
type="button"
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
onClick={onAppendSecret}
>
<FontAwesomeIcon icon={faPlus} />
<span className="ml-2 w-20">Add Secret</span>
</button>
</td>
</tr>
)}
</tbody>
</table>
</TableContainer>
</DndContext>
)}
<PitDrawer
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
@@ -881,6 +1032,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
/>
</ModalContent>
</Modal>
<Modal
isOpen={popUp?.addSecretImport?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
>
<ModalContent
title="Add Secret Link"
subTitle="To inherit secrets from another environment or folder"
>
<SecretImportForm
environments={currentWorkspace?.environments}
onCreate={handleSecretImportCreate}
/>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.deleteFolder.isOpen}
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
@@ -888,6 +1053,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
onDeleteApproved={handleFolderDelete}
/>
<DeleteActionModal
isOpen={popUp.deleteSecretImport.isOpen}
deleteKey="unlink"
title="Do you want to remove this secret import?"
subTitle={`This will unlink secrets from environment ${
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
onDeleteApproved={handleSecretImportDelete}
/>
<Modal
isOpen={popUp?.compareSecrets?.isOpen}
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}
@@ -67,7 +67,7 @@ export const FolderSection = ({
ariaLabel="delete"
onClick={() => handleFolderDelete(id, name)}
>
<FontAwesomeIcon icon={faXmark} />
<FontAwesomeIcon icon={faXmark} size="lg" />
</IconButton>
</Tooltip>
</div>
@@ -0,0 +1,86 @@
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
type Props = {
onCreate: (environment: string, secretPath: string) => Promise<void>;
environments?: Array<{ slug: string; name: string }>;
};
const formSchema = yup.object({
environment: yup.string().required().label("Environment").trim(),
secretPath: yup
.string()
.required()
.label("Secret Path")
.trim()
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
)
});
type TFormData = yup.InferType<typeof formSchema>;
export const SecretImportForm = ({ onCreate, environments = [] }: Props): JSX.Element => {
const {
control,
reset,
formState: { isSubmitting },
handleSubmit
} = useForm<TFormData>({
resolver: yupResolver(formSchema)
});
const onSubmit = async ({ environment, secretPath }: TFormData) => {
await onCreate(environment, secretPath);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="environment"
defaultValue={environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
@@ -0,0 +1 @@
export { SecretImportForm } from "./SecretImportForm";
@@ -0,0 +1,157 @@
import { useEffect } from "react";
import { useSortable } from "@dnd-kit/sortable";
import {
faFileImport,
faFolder,
faKey,
faUpDown,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks/useToggle";
type Props = {
onDelete: (environment: string, secretPath: string) => void;
importedEnv: string;
importedSecPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
};
// to show the environment and folder icon
export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: string }) => (
<div className="inline-flex items-center space-x-2">
<div style={{ minWidth: "96px" }}>{env || "-"}</div>
{secretPath && (
<div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2">
<FontAwesomeIcon icon={faFolder} size="lg" className="text-primary-700" />
<span>{secretPath}</span>
</div>
)}
</div>
);
export const SecretImportItem = ({
importedEnv,
importedSecPath,
onDelete,
importedSecrets = []
}: Props) => {
const [isExpanded, setIsExpanded] = useToggle();
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
id: `${importedEnv}-${importedSecPath}`
});
const { currentWorkspace } = useWorkspace();
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
useEffect(() => {
if (isDragging) {
setIsExpanded.off();
}
}, [isDragging]);
const style = {
transform: transform ? `translateY(${transform.y ? Math.round(transform.y) : 0}px)` : "",
transition
};
return (
<>
<tr
ref={setNodeRef}
style={style}
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
onClick={() => setIsExpanded.toggle()}
>
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
<FontAwesomeIcon icon={faFileImport} className="text-primary-700" />
</td>
<td
colSpan={2}
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
style={{ paddingTop: "0", paddingBottom: "0" }}
>
<div className="flex-grow p-2">
<EnvFolderIcon env={rowEnv?.name || ""} secretPath={importedSecPath} />
</div>
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Change Order" className="capitalize">
<IconButton
size="md"
colorSchema="primary"
variant="plain"
ariaLabel="expand"
{...attributes}
{...listeners}
>
<FontAwesomeIcon icon={faUpDown} size="lg" />
</IconButton>
</Tooltip>
</div>
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete" className="capitalize">
<IconButton
size="md"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
onClick={(evt) => {
evt.stopPropagation();
onDelete(importedEnv, importedSecPath);
}}
>
<FontAwesomeIcon icon={faXmark} size="lg" />
</IconButton>
</Tooltip>
</div>
</div>
</td>
</tr>
<tr>
{isExpanded && !isDragging && (
<td colSpan={3}>
<div className="rounded-md bg-bunker-700 p-4 pb-6">
<div className="mb-2 text-lg font-medium">Secrets Imported</div>
<TableContainer>
<table className="secret-table">
<thead>
<tr>
<td style={{ padding: "0.25rem 1rem" }}>Key</td>
<td style={{ padding: "0.25rem 1rem" }}>Value</td>
<td style={{ padding: "0.25rem 1rem" }}>Override</td>
</tr>
</thead>
<tbody>
{importedSecrets?.length === 0 && (
<tr>
<td colSpan={3}>
<EmptyState title="No secrets found" icon={faKey} />
</td>
</tr>
)}
{importedSecrets.map(({ key, value, overriden }, index) => (
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{key}
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{value}
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
</td>
</tr>
))}
</tbody>
</table>
</TableContainer>
</div>
</td>
)}
</tr>
</>
);
};
@@ -0,0 +1,93 @@
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useWorkspace } from "@app/context";
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { SecretImportItem } from "./SecretImportItem";
type TImportedSecrets = Array<{
environment: string;
secretPath: string;
folderId: string;
secrets: DecryptedSecret[];
}>;
const SECRET_IN_DASHBOARD = "Present In Dashboard";
export const computeImportedSecretRows = (
importedSecEnv: string,
importedSecPath: string,
importSecrets: TImportedSecrets = [],
secrets: DecryptedSecret[] = [],
environments: { name: string; slug: string }[] = []
) => {
const importedSecIndex = importSecrets.findIndex(
({ secretPath, environment }) =>
secretPath === importedSecPath && importedSecEnv === environment
);
if (importedSecIndex === -1) return [];
const importedSec = importSecrets[importedSecIndex];
const overridenSec: Record<string, { env: string; secretPath: string }> = {};
const envSlug2Name: Record<string, string> = {};
environments.forEach((el) => {
envSlug2Name[el.slug] = el.name;
});
for (let i = importedSecIndex + 1; i < importSecrets.length; i += 1) {
importSecrets[i].secrets.forEach((el) => {
overridenSec[el.key] = {
env: envSlug2Name?.[importSecrets[i].environment] || "unknown",
secretPath: importSecrets[i].secretPath
};
});
}
secrets.forEach((el) => {
overridenSec[el.key] = { env: SECRET_IN_DASHBOARD, secretPath: "" };
});
return importedSec.secrets.map(({ key, value }) => ({
key,
value,
overriden: overridenSec?.[key]
}));
};
type Props = {
secrets?: DecryptedSecret[];
importedSecrets?: TImportedSecrets;
onSecretImportDelete: (env: string, secPath: string) => void;
items: { id: string; environment: string; secretPath: string }[];
};
export const SecretImportSection = ({
secrets = [],
importedSecrets = [],
onSecretImportDelete,
items = []
}: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
return (
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
<SecretImportItem
key={id}
importedEnv={importSecEnv}
importedSecrets={computeImportedSecretRows(
importSecEnv,
impSecPath,
importedSecrets,
secrets,
environments
)}
onDelete={onSecretImportDelete}
importedSecPath={impSecPath}
/>
))}
</SortableContext>
);
};
@@ -0,0 +1 @@
export { SecretImportSection } from "./SecretImportSection";
@@ -407,7 +407,7 @@ export const SecretInputRow = memo(
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
size="md"
size="lg"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
@@ -92,6 +92,10 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
case "codefresh":
link = `${window.location.origin}/integrations/codefresh/authorize`;
break;
default:
break;
}
@@ -48,6 +48,7 @@ export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
useGetWorkspaceAuthorizations(
workspaceId,
@@ -31,6 +31,8 @@ export const CloudIntegrationSection = ({
const isEmpty = !isLoading && !cloudIntegrations?.length;
const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
return (
<div>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
@@ -43,7 +45,7 @@ export const CloudIntegrationSection = ({
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
))}
{!isLoading &&
cloudIntegrations?.map((cloudIntegration) => (
sortedCloudIntegrations?.map((cloudIntegration) => (
<div
onKeyDown={() => null}
role="button"
@@ -12,6 +12,8 @@ type Props = {
export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
const { t } = useTranslation();
const sortedFrameworks = frameworks.sort((a, b) => a.name.localeCompare(b.name));
return (
<>
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
@@ -22,7 +24,7 @@ export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
className="mx-6 mt-4 grid grid-flow-dense gap-3"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }}
>
{frameworks.map((framework) => (
{sortedFrameworks.map((framework) => (
<a
key={`framework-integration-${framework.slug}`}
href={framework.docsLink}
@@ -32,7 +32,6 @@ export const IntegrationsSection = ({
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
return (
<div className="mb-8">
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
@@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
export const RiskStatusSelection = ({riskId, currentSelection}: {riskId: any, currentSelection: any }) => {
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
useEffect(()=>{
if (currentSelection !== selectedRiskStatus){
const updateSelection = async () =>{
await updateRiskStatus(String(localStorage.getItem("orgData.id")), riskId, selectedRiskStatus)
}
updateSelection()
}
},[selectedRiskStatus])
return (
<select
value={selectedRiskStatus}
onChange={(e) => setSelectedRiskStatus(e.target.value)}
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option>Unresolved</option>
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive, resolved</option>
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
</select>
);
}
@@ -0,0 +1,99 @@
import { useEffect, useState } from "react";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr} from "@app/components/v2";
import timeSince from "@app/ee/utilities/timeSince";
import getRisksByOrganization, { GitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization";
import { RiskStatusSelection } from "./RiskStatusSelection";
export const SecretScanningLogsTable = () => {
const [isLoading, setIsLoading] = useState(false);
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
useEffect(() => {
const fetchRisks = async () => {
setIsLoading(true);
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
setGitRisks(risks);
setIsLoading(false);
}
fetchRisks();
},[])
return (
<TableContainer className="mt-8">
<Table>
<THead>
<Tr>
<Th className="flex-1">Date</Th>
<Th className="flex-1">Secret Type</Th>
<Th className="flex-1">View Risk</Th>
<Th className="flex-1">Info</Th>
<Th className="flex-1">Status</Th>
<Th className="flex-1">Action</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading && gitRisks && gitRisks?.map((risk) => {
return (
<Tr key={risk.ruleID} className="h-10">
<Td>{timeSince(new Date(risk.createdAt))}</Td>
<Td>{risk.ruleID}</Td>
<Td>
<a
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
target="_blank"
className="text-red-500" rel="noreferrer"
>
View Exposed Secret
</a>
</Td>
<Td>
<div className="font-bold">
<a href={`https://github.com/${risk.repositoryFullName}`}>
{risk.repositoryFullName}
</a>
</div>
<div className="text-xs">
<span>{risk.file}</span><br/>
<br/>
<span className="font-bold">{risk.author}</span><br/>
<span>{risk.email}</span>
</div>
</Td>
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
<Td>
<RiskStatusSelection riskId={risk._id} currentSelection={risk.status}/>
</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={7} key="gitRisks" />}
{!isLoading && gitRisks && gitRisks?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState
title="No risks detected."
icon={faCheck}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1 @@
export { SecretScanningLogsTable } from "./SecretScanningLogsTable";
@@ -7,8 +7,8 @@ import {
export const BillingSettingsPage = () => {
const { t } = useTranslation();
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full px-6">
<div className="max-w-7xl w-full">
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
<div />
@@ -15,7 +15,7 @@ export const ChangeLanguageSection = () => {
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
{t("settings.personal.change-language")}
</p>
@@ -3,10 +3,12 @@ import { ChangePasswordSection } from "../ChangePasswordSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SecuritySection } from "../SecuritySection";
import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
export const PersonalSecurityTab = () => {
return (
<div>
<UserNameSection />
<ChangeLanguageSection />
<SecuritySection />
<SessionsSection />
@@ -12,7 +12,7 @@ const tabs = [
export const PersonalTabGroup = () => {
return (
<Tab.Group>
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
<Tab.List className="mb-4 border-b-2 border-mineshaft-800 w-full">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (

Some files were not shown because too many files have changed in this diff Show More