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 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} 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: gamma-deployment:
name: Deploy to gamma name: Deploy to gamma
runs-on: ubuntu-latest runs-on: ubuntu-latest
+1 -7
View File
@@ -10,7 +10,6 @@
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"rules": { "rules": {
"@typescript-eslint/no-empty-function": "off",
"no-console": 2, "no-console": 2,
"quotes": [ "quotes": [
"error", "error",
@@ -34,11 +33,6 @@
"argsIgnorePattern": "^_" "argsIgnorePattern": "^_"
} }
], ],
"sort-imports": [ "sort-imports": 1
"error",
{
"ignoreDeclarationSort": true
}
]
} }
} }
+4
View File
@@ -19,6 +19,10 @@ RUN npm ci --only-production
COPY --from=build /app . 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 \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js 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": "^0.6.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2", "rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@@ -104,6 +105,7 @@
"jest-junit": "^15.0.0", "jest-junit": "^15.0.0",
"nodemon": "^2.0.19", "nodemon": "^2.0.19",
"npm": "^8.19.3", "npm": "^8.19.3",
"smee-client": "^1.2.3",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
+5
View File
@@ -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 getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical"; 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 () => { export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue; const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : 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 workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController"; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController"; import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController";
export { export {
authController, authController,
@@ -33,5 +34,6 @@ export {
userController, userController,
workspaceController, workspaceController,
secretScanningController, 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 { UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation"; import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
import { MembershipOrg } from "../../models"; 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) => { export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex"); 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) => { export const getRisksForOrganization = async (req: Request, res: Response) => {
const { organizationId } = req.params 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({ res.json({
risks: risks risks: risks
}) })
@@ -81,9 +81,11 @@ export const getRisksForOrganization = async (req: Request, res: Response) => {
export const updateRisksStatus = async (req: Request, res: Response) => { export const updateRisksStatus = async (req: Request, res: Response) => {
const { riskId } = req.params const { riskId } = req.params
const { status } = req.body const { status } = req.body
const risks = await GitRisks.findByIdAndUpdate(riskId, { const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false
sttaus: status const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status,
isResolved: isRiskResolved
}).lean() }).lean()
res.json(risks) res.json(risk)
} }
@@ -191,14 +191,21 @@ export const deleteWorkspaceEnvironment = async (
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
}); });
await ServiceToken.deleteMany({
workspace: workspaceId, // await ServiceToken.deleteMany({
environment: environmentSlug, // workspace: workspaceId,
}); // environment: environmentSlug,
await ServiceTokenData.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({ await Integration.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
@@ -35,6 +35,7 @@ import {
} from "../../services/FolderService"; } from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets"; import { isValidScope } from "../../helpers/secrets";
import path from "path"; import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService";
/** /**
* Peform a batch of any specified CUD secret operations * 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; let { folderId } = req.query;
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment 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"); 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 channel = getChannelFromUserAgent(req.headers["user-agent"]);
const readAction = await EELogService.createAction({ const readAction = await EELogService.createAction({
@@ -868,7 +875,8 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
return res.status(200).send({ 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. * Return organizations that the current user is part of.
* @param req * @param req
@@ -5,6 +5,10 @@ import { eventPushSecrets } from "../../events";
import { BotService } from "../../services"; import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets"; import { repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; 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 * 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 workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string; const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -28,13 +33,38 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId) 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({ return res.status(200).send({
secrets: secrets.map((secret) => { secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({ const rep = repackageSecretToRaw({
secret, secret,
key key
}); });
return rep; return rep;
}) })
}); });
@@ -232,6 +262,7 @@ export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string; const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -240,6 +271,24 @@ export const getSecrets = async (req: Request, res: Response) => {
authData: req.authData 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({ return res.status(200).send({
secrets secrets
}); });
+28 -3
View File
@@ -5,11 +5,12 @@ import express from "express";
require("express-async-errors"); require("express-async-errors");
import helmet from "helmet"; import helmet from "helmet";
import cors from "cors"; import cors from "cors";
import { DatabaseService } from "./services"; import { DatabaseService, GithubSecretScanningService } from "./services";
import { EELicenseService } from "./ee/services"; import { EELicenseService } from "./ee/services";
import { setUpHealthEndpoint } from "./services/health"; import { setUpHealthEndpoint } from "./services/health";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express"); import swaggerUi = require("swagger-ui-express");
import { Probot, createNodeMiddleware } from "probot";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json"); const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -42,7 +43,8 @@ import {
userAction as v1UserActionRouter, userAction as v1UserActionRouter,
user as v1UserRouter, user as v1UserRouter,
webhooks as v1WebhooksRouter, webhooks as v1WebhooksRouter,
workspace as v1WorkspaceRouter workspace as v1WorkspaceRouter,
secretImport as v1SecretImportRouter
} from "./routes/v1"; } from "./routes/v1";
import { import {
auth as v2AuthRouter, auth as v2AuthRouter,
@@ -67,10 +69,12 @@ import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger"; import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors"; import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler"; 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"; import { setup } from "./utils/setup";
const SmeeClient = require('smee-client') // eslint-disable-line
const main = async () => { const main = async () => {
await setup(); await setup();
await EELicenseService.initGlobalFeatureSet(); 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") { if ((await getNodeEnv()) === "production") {
// enable app-wide rate-limiting + helmet security // enable app-wide rate-limiting + helmet security
// in production // in production
@@ -131,6 +155,7 @@ const main = async () => {
app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter); app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
+39
View File
@@ -32,6 +32,9 @@ import {
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL
} from "../variables"; } from "../variables";
interface App { interface App {
@@ -145,6 +148,11 @@ const getApps = async ({
accountId: accessId accountId: accessId
}) })
break; break;
case INTEGRATION_CODEFRESH:
apps = await getAppsCodefresh({
accessToken,
});
break;
} }
return apps; return apps;
@@ -721,4 +729,35 @@ const getAppsCloudflarePages = async ({
return apps; 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 }; export { getApps };
+71 -1
View File
@@ -42,6 +42,8 @@ import {
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL
} from "../variables"; } from "../variables";
import { standardRequest } from "../config/request"; import { standardRequest } from "../config/request";
@@ -178,6 +180,34 @@ const syncSecrets = async ({
accessToken, accessToken,
}); });
break; 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: case INTEGRATION_CHECKLY:
await syncSecretsCheckly({ await syncSecretsCheckly({
integration, integration,
@@ -202,6 +232,13 @@ const syncSecrets = async ({
accessToken accessToken
}); });
break; break;
case INTEGRATION_CODEFRESH:
await syncSecretsCodefresh({
integration,
secrets,
accessToken,
});
break;
} }
}; };
@@ -1898,7 +1935,7 @@ const syncSecretsCloudflarePages = async ({
} }
) )
) )
.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 // copy the secrets object, so we can set deleted keys to null
const secretsObj: any = { ...secrets }; const secretsObj: any = { ...secrets };
@@ -1937,4 +1974,37 @@ const syncSecretsCloudflarePages = async ({
); );
} }
/**
* 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 }; export { syncSecrets };
+4
View File
@@ -25,6 +25,7 @@ export type GitRisks = {
tags: string[]; tags: string[];
ruleID: string; ruleID: string;
fingerprint: string; fingerprint: string;
fingerPrintWithoutCommitId: string
isFalsePositive: boolean; // New field for marking risks as false positives isFalsePositive: boolean; // New field for marking risks as false positives
isResolved: boolean; // New field for marking risks as resolved isResolved: boolean; // New field for marking risks as resolved
@@ -94,6 +95,9 @@ const gitRisks = new Schema<GitRisks>({
type: String, type: String,
unique: true unique: true
}, },
fingerPrintWithoutCommitId: {
type: String,
},
isFalsePositive: { isFalsePositive: {
type: Boolean, type: Boolean,
default: false default: false
+3
View File
@@ -24,6 +24,7 @@ import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
import APIKeyData, { IAPIKeyData } from "./apiKeyData"; import APIKeyData, { IAPIKeyData } from "./apiKeyData";
import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail"; import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
import TokenVersion, { ITokenVersion } from "./tokenVersion"; import TokenVersion, { ITokenVersion } from "./tokenVersion";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./gitRisks";
export { export {
AuthProvider, AuthProvider,
@@ -79,4 +80,6 @@ export {
ILoginSRPDetail, ILoginSRPDetail,
TokenVersion, TokenVersion,
ITokenVersion, ITokenVersion,
GitRisks,
STATUS_RESOLVED_FALSE_POSITIVE
}; };
+4 -1
View File
@@ -18,6 +18,7 @@ import {
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_CODEFRESH
} from "../variables"; } from "../variables";
export interface IIntegration { export interface IIntegration {
@@ -54,7 +55,8 @@ export interface IIntegration {
| "supabase" | "supabase"
| "checkly" | "checkly"
| "hashicorp-vault" | "hashicorp-vault"
| "cloudflare-pages"; | "cloudflare-pages"
| "codefresh";
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -144,6 +146,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH
], ],
required: true, required: true,
}, },
+4 -2
View File
@@ -19,13 +19,14 @@ import {
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL INTEGRATION_VERCEL,
INTEGRATION_CODEFRESH
} from "../variables"; } from "../variables";
export interface IIntegrationAuth extends Document { export interface IIntegrationAuth extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: 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; teamId: string;
accountId: string; accountId: string;
url: string; url: string;
@@ -71,6 +72,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH
], ],
required: true, 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 express, { Request, Response } from "express";
import { getSmtpConfigured } from "../../config"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
const router = express.Router(); const router = express.Router();
@@ -10,6 +10,7 @@ router.get(
date: new Date(), date: new Date(),
message: "Ok", message: "Ok",
emailConfigured: await getSmtpConfigured(), 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 secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning"; import secretScanning from "./secretScanning";
import webhooks from "./webhook"; import webhooks from "./webhook";
import secretImport from "./secretImport";
export { export {
signup, signup,
@@ -37,5 +38,6 @@ export {
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
secretScanning, 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("organizationId").exists().trim(),
param("riskId").exists().trim(), param("riskId").exists().trim(),
body("status").exists(),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED],
+1
View File
@@ -127,6 +127,7 @@ router.get(
query("tagSlugs"), query("tagSlugs"),
query("folderId").default("root").isString().trim(), query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(), query("secretPath").optional().isString().trim(),
query("include_imports").optional().default(false).isBoolean(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
+10
View File
@@ -29,6 +29,16 @@ router.patch(
usersController.updateMyMfaEnabled usersController.updateMyMfaEnabled
); );
router.patch(
"/me/name",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("firstName").exists(),
validateRequest,
usersController.updateName
);
router.get( router.get(
"/me/organizations", "/me/organizations",
requireAuth({ requireAuth({
+33 -36
View File
@@ -1,10 +1,6 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import { body, param, query } from "express-validator"; import { body, param, query } from "express-validator";
import { secretsController } from "../../controllers/v3"; import { secretsController } from "../../controllers/v3";
import { import {
@@ -17,7 +13,7 @@ import {
PERMISSION_READ_SECRETS, PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../../variables"; } from "../../variables";
router.get( router.get(
@@ -25,14 +21,15 @@ router.get(
query("workspaceId").exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(), query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(), query("secretPath").default("/").isString().trim(),
query("include_imports").optional().isBoolean().default(false),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -40,7 +37,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.getSecretsRaw secretsController.getSecretsRaw
); );
@@ -58,8 +55,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -67,7 +64,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.getSecretByNameRaw secretsController.getSecretByNameRaw
); );
@@ -86,8 +83,8 @@ router.post(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -95,7 +92,7 @@ router.post(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.createSecretRaw secretsController.createSecretRaw
); );
@@ -114,8 +111,8 @@ router.patch(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -123,7 +120,7 @@ router.patch(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.updateSecretByNameRaw secretsController.updateSecretByNameRaw
); );
@@ -141,8 +138,8 @@ router.delete(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -150,7 +147,7 @@ router.delete(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.deleteSecretByNameRaw secretsController.deleteSecretByNameRaw
); );
@@ -166,8 +163,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -175,7 +172,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
@@ -201,8 +198,8 @@ router.post(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -210,7 +207,7 @@ router.post(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.createSecret secretsController.createSecret
); );
@@ -228,15 +225,15 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query", locationWorkspaceId: "query",
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true
}), }),
secretsController.getSecretByName secretsController.getSecretByName
); );
@@ -257,8 +254,8 @@ router.patch(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -266,7 +263,7 @@ router.patch(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.updateSecretByName secretsController.updateSecretByName
); );
@@ -284,8 +281,8 @@ router.delete(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -293,7 +290,7 @@ router.delete(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.deleteSecretByName secretsController.deleteSecretByName
); );
@@ -1,15 +1,15 @@
import { Probot } from "probot"; import { Probot } from "probot";
import { exec } from "child_process"; import { exec } from "child_process";
import { writeFile, readFile, rm, mkdir, } from "fs"; import { mkdir, readFile, rm, writeFile } from "fs";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { join } from "path" import { join } from "path"
import mongoose from "mongoose"; import GitRisks from "../models/gitRisks";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./models/gitRisks"; import GitAppOrganizationInstallation from "../models/gitAppOrganizationInstallation";
import GitAppOrganizationInstallation from "./models/gitAppOrganizationInstallation"; import MembershipOrg from "../models/membershipOrg";
import { sendMail, setTransporter } from "./helper/nodemailer"; import { ADMIN, OWNER } from "../variables";
import { initSmtp } from "./service/smtp"; import User from "../models/user";
import MembershipOrg, { ADMIN, OWNER } from "./models/membershipOrg"; import { sendMail } from "../helpers";
import User from "./models/user"; import TelemetryService from "./TelemetryService";
type SecretMatch = { type SecretMatch = {
Description: string; Description: string;
@@ -30,19 +30,10 @@ type SecretMatch = {
Tags: string[]; Tags: string[];
RuleID: string; RuleID: string;
Fingerprint: string; Fingerprint: string;
FingerPrintWithoutCommitId: string
}; };
export = async (app: Probot) => { export default 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)
})
app.on("installation.deleted", async (context) => { app.on("installation.deleted", async (context) => {
const { payload } = context; const { payload } = context;
const { installation, repositories } = payload; const { installation, repositories } = payload;
@@ -50,18 +41,24 @@ export = async (app: Probot) => {
await GitRisks.deleteMany({ installationId: installation.id }) await GitRisks.deleteMany({ installationId: installation.id })
await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id }) await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id })
} else { } else {
if (repositories) {
for (const repository of repositories) { for (const repository of repositories) {
await GitRisks.deleteMany({ repositoryId: repository.id }) await GitRisks.deleteMany({ repositoryId: repository.id })
} }
} }
}
}) })
app.on("push", async (context) => { app.on("push", async (context) => {
const { payload } = context; const { payload } = context;
const { commits, repository, installation, pusher } = payload; 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) { if (!installationLinkToOrgExists) {
return return
} }
@@ -78,31 +75,30 @@ export = async (app: Probot) => {
}); });
const data: any = fileContentsResponse.data; 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) { for (const finding of findings) {
const fingerPrint = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}` const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
finding.Fingerprint = fingerPrint const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`
finding.Fingerprint = fingerPrintWithCommitId
finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId
finding.Commit = commit.id finding.Commit = commit.id
finding.File = filepath finding.File = filepath
finding.Author = commit.author.name finding.Author = commit.author.name
finding.Email = commit.author.email finding.Email = commit?.author?.email ? commit?.author?.email : ""
allFindingsByFingerprint[fingerPrint] = finding
allFindingsByFingerprint[fingerPrintWithCommitId] = finding
} }
} catch (error) { } catch (error) {
console.error(`Error fetching content for ${filepath}`, error); console.error(`Error fetching content for ${filepath}`, error); // eslint-disable-line
} }
} }
} }
// change to update // change to update
const noneFalsePositiveFindings = {}
for (const key in allFindingsByFingerprint) { for (const key in allFindingsByFingerprint) {
const risk = await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint }, const risk = await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint },
{ {
@@ -114,11 +110,6 @@ export = async (app: Probot) => {
}, { }, {
upsert: true upsert: true
}).lean() }).lean()
if (risk.status != STATUS_RESOLVED_FALSE_POSITIVE) {
noneFalsePositiveFindings[key] = { ...convertKeysToLowercase(allFindingsByFingerprint[key]) }
}
} }
// get emails of admins // get emails of admins
const adminsOfWork = await MembershipOrg.find({ const adminsOfWork = await MembershipOrg.find({
@@ -137,39 +128,30 @@ export = async (app: Probot) => {
const adminOrOwnerEmails = userEmails.map(userObject => userObject.email) const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
// TODO if (Object.keys(allFindingsByFingerprint).length) {
// don't notify if the risk is marked as false positive
await sendMail({ await sendMail({
template: "secretLeakIncident.handlebars", template: "secretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`, subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
recipients: [pusher.email, ...adminOrOwnerEmails], recipients: usersToNotify,
substitutions: { substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length, numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email, pusher_email: pusher.email,
pusher_name: pusher.name pusher_name: pusher.name
} }
}); });
}); }
app.on(['pull_request.opened', 'pull_request.synchronize'], async (context) => { const postHogClient = await TelemetryService.getPostHogClient();
const { payload } = context; if (postHogClient) {
const { pull_request } = payload postHogClient.capture({
if (false) { event: "cloud secret scan",
const check = { distinctId: pusher.email,
owner: pull_request.head.repo.owner.login, properties: {
repo: pull_request.head.repo.name, numberOfCommitsScanned: commits.length,
name: 'Secret Detection', numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
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);
} }
}); });
}; };
@@ -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 { function convertKeysToLowercase<T>(obj: T): T {
const convertedObj = {} as T; const convertedObj = {} as T;
for (const key in obj) { for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) { if (Object.prototype.hasOwnProperty.call(obj, key)) {
const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1); 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;
};
+2
View File
@@ -7,6 +7,7 @@ import EventService from "./EventService";
import IntegrationService from "./IntegrationService"; import IntegrationService from "./IntegrationService";
import TokenService from "./TokenService"; import TokenService from "./TokenService";
import SecretService from "./SecretService"; import SecretService from "./SecretService";
import GithubSecretScanningService from "./GithubSecretScanningService"
export { export {
TelemetryService, TelemetryService,
@@ -17,4 +18,5 @@ export {
IntegrationService, IntegrationService,
TokenService, TokenService,
SecretService, 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_CHECKLY = "checkly";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_CODEFRESH = "codefresh";
export const INTEGRATION_SET = new Set([ export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
@@ -41,7 +42,8 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH
]); ]);
// integration types // 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_LARAVELFORGE_API_URL = "https://forge.laravel.com";
export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.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_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api";
export const getIntegrationOptions = async () => { export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
@@ -245,7 +248,16 @@ export const getIntegrationOptions = async () => {
type: "pat", type: "pat",
clientId: "", clientId: "",
docsLink: "" docsLink: ""
} },
{
name: "Codefresh",
slug: "codefresh",
image: "Codefresh.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
},
] ]
return INTEGRATION_OPTIONS; return INTEGRATION_OPTIONS;
+4
View File
@@ -235,6 +235,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId) SetQueryParam("workspaceId", request.WorkspaceId)
if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true")
}
if request.SecretPath != "" { if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath) httpRequest.SetQueryParam("secretPath", request.SecretPath)
} }
+14 -3
View File
@@ -275,10 +275,10 @@ type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"` Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"` SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
} }
type GetEncryptedSecretsV3Response struct { type EncryptedSecretV3 struct {
Secrets []struct {
ID string `json:"_id"` ID string `json:"_id"`
Version int `json:"version"` Version int `json:"version"`
Workspace string `json:"workspace"` Workspace string `json:"workspace"`
@@ -305,7 +305,18 @@ type GetEncryptedSecretsV3Response struct {
V int `json:"__v"` V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"` }
type ImportedSecretV3 struct {
Environment string `json:"environment"`
FolderId string `json:"folderId"`
SecretPath string `json:"secretPath"`
Secrets []EncryptedSecretV3 `json:"secrets"`
}
type GetEncryptedSecretsV3Response struct {
Secrets []EncryptedSecretV3 `json:"secrets"`
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
} }
type CreateSecretV3Request struct { type CreateSecretV3Request struct {
+7 -1
View File
@@ -87,7 +87,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") 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 { 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") 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().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().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("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().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("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
+7 -1
View File
@@ -54,12 +54,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err) util.HandleError(err)
} }
includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err)
}
tagSlugs, err := cmd.Flags().GetString("tags") tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") 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 { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
@@ -647,6 +652,7 @@ func init() {
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") 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.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("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.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd) rootCmd.AddCommand(secretsCmd)
+1
View File
@@ -65,4 +65,5 @@ type GetAllSecretsParameters struct {
TagSlugs string TagSlugs string
WorkspaceId string WorkspaceId string
SecretsPath string SecretsPath string
IncludeImport bool
} }
+53 -9
View File
@@ -17,7 +17,7 @@ import (
"github.com/rs/zerolog/log" "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) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -48,6 +48,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: environment, Environment: environment,
SecretPath: secretPath, SecretPath: secretPath,
IncludeImport: includeImports,
}) })
if err != nil { 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") 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 { if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) 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 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 := resty.New()
httpClient.SetAuthToken(JTWToken). httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -116,6 +124,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
getSecretsRequest := api.GetEncryptedSecretsV3Request{ getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId, WorkspaceId: workspaceId,
Environment: environmentName, Environment: environmentName,
IncludeImport: includeImports,
// TagSlugs: tagSlugs, // TagSlugs: tagSlugs,
} }
@@ -124,19 +133,53 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
} }
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest) encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) 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 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) { func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
var infisicalToken string var infisicalToken string
if params.InfisicalToken == "" { 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) 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) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -199,7 +243,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
} else { } else {
log.Debug().Msg("Trying to fetch secrets using service token") 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 return secretsToReturn, errorToReturn
@@ -427,9 +471,9 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn 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{} plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets.Secrets { for _, secret := range encryptedSecrets {
// Decrypt key // Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV) key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil { if err != nil {
-25
View File
@@ -58,31 +58,6 @@ services:
networks: networks:
- infisical-dev - 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: mongo:
image: mongo image: mongo
container_name: infisical-dev-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 ## 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). - Added the [intergation with Laravel Forge](https://infisical.com/docs/integrations/cloud/laravel-forge).
- Redesigned the project/organization experience. - 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" 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 ## Installation
@@ -57,7 +58,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash ```bash
apk update && sudo apk add infisical 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>
<Tab title="RedHat/CentOs/Amazon"> <Tab title="RedHat/CentOs/Amazon">
Add Infisical repository Add Infisical repository
@@ -71,7 +75,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash ```bash
sudo yum install infisical 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>
<Tab title="Debian/Ubuntu"> <Tab title="Debian/Ubuntu">
Add Infisical repository Add Infisical repository
@@ -86,7 +93,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash ```bash
sudo apt-get update && sudo apt-get install -y infisical 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>
<Tab title="Arch Linux"> <Tab title="Arch Linux">
Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin) 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 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> </Tab>
</Tabs> </Tabs>
+1 -1
View File
@@ -8,7 +8,7 @@ The distinguishing factor, however, is the authentication method used.
<Tabs> <Tabs>
<Tab title="Local development"> <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 ```bash
infisical login 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 Store secrets like API keys, database credentials, environment variables with Infisical
</Card> </Card>
## Integrate with Infisical ## Access secrets
<CardGroup cols={2}> <CardGroup cols={2}>
<Card href="../../cli/overview" title="Command Line Interface (CLI)" icon="square-terminal" color="#3775a9"> <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 Fetch secrets with any programming language on demand
</Card> </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 Inject secrets into Docker containers
</Card> </Card>
<Card <Card
href="/documentation/getting-started/kubernetes" href="../../integrations/platforms/kubernetes"
title="Kubernetes" title="Kubernetes"
icon="server" icon="server"
color="#3775a9" color="#3775a9"
@@ -1,5 +1,5 @@
--- ---
title: "Reference Secrets" title: "Reference/Import Secrets"
description: "How to use reference secrets in Infisical" 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 ## 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. 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. 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. 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 ## 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) ![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. 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. 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. 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." description: "How to use Infisical to inject environment variables into a Docker container."
--- ---
Prerequisites: 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.
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Add the Infisical CLI to your Dockerfile ## Add the Infisical CLI to your Dockerfile
@@ -33,6 +32,10 @@ Prerequisites:
``` ```
</Tab> </Tab>
</Tabs> </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 ## Modify the start command in your Dockerfile
@@ -48,18 +51,17 @@ CMD ["infisical", "run", "--", "npm", "run", "start"]
CMD ["infisical", "run", "--command", "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 ```bash
docker run --env INFISICAL_TOKEN=[token] [DOCKER-IMAGE]... 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/introduction",
"documentation/getting-started/platform", "documentation/getting-started/platform",
"documentation/getting-started/sdks", "documentation/getting-started/sdks",
"documentation/getting-started/docker", "integrations/platforms/kubernetes",
"documentation/getting-started/kubernetes", "integrations/platforms/docker-intro",
"documentation/getting-started/api" "documentation/getting-started/api"
] ]
}, },
@@ -187,7 +187,9 @@
{ {
"group": "Docker", "group": "Docker",
"pages": [ "pages": [
"integrations/platforms/docker-intro",
"integrations/platforms/docker", "integrations/platforms/docker",
"integrations/platforms/docker-pass-envs",
"integrations/platforms/docker-compose" "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. Choose from a variety of deployment options listed below to get started.
<Card <Card
title="Kubernetes" title="Kubernetes (recommended)"
color="#ea5a0c" color="#ea5a0c"
href="deployment-options/kubernetes-helm" href="deployment-options/kubernetes-helm"
> >
+109
View File
@@ -5,6 +5,9 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "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/css": "^11.10.0",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
@@ -2468,6 +2471,68 @@
"node": ">=10.0.0" "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": { "node_modules/@emotion/babel-plugin": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@@ -24491,6 +24556,50 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"dev": true "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": { "@emotion/babel-plugin": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "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" "build-storybook": "storybook build"
}, },
"dependencies": { "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/css": "^11.10.0",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
+2 -1
View File
@@ -20,7 +20,8 @@ const integrationSlugNameMapping: Mapping = {
'supabase': 'Supabase', 'supabase': 'Supabase',
'checkly': 'Checkly', 'checkly': 'Checkly',
'hashicorp-vault': 'Vault', 'hashicorp-vault': 'Vault',
'cloudflare-pages': 'Cloudflare Pages' 'cloudflare-pages': 'Cloudflare Pages',
'codefresh': 'Codefresh'
} }
const envMapping: Mapping = { 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 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="flex flex-row items-center py-4">
<div className="text-sm text-bunker-300">{title}</div> <div className="text-sm text-bunker-300">{title}</div>
<div>{children}</div> <div>{children}</div>
+1
View File
@@ -7,6 +7,7 @@ export * from "./keys";
export * from "./organization"; export * from "./organization";
export * from "./ssoConfig"; export * from "./ssoConfig";
export * from "./secretFolders"; export * from "./secretFolders";
export * from "./secretImports";
export * from "./secrets"; export * from "./secrets";
export * from "./secretSnapshots"; export * from "./secretSnapshots";
export * from "./serviceAccounts"; export * from "./serviceAccounts";
@@ -5,5 +5,5 @@ export {
useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices, useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams, useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches useGetIntegrationAuthVercelBranches,
} from "./queries"; } from "./queries";
@@ -19,6 +19,7 @@ const integrationAuthKeys = {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const, }) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthRailwayEnvironments: ({ getIntegrationAuthRailwayEnvironments: ({
integrationAuthId, integrationAuthId,
appId appId
@@ -64,6 +65,7 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
return data.teams; return data.teams;
}; };
const fetchIntegrationAuthVercelBranches = async ({ const fetchIntegrationAuthVercelBranches = async ({
integrationAuthId, integrationAuthId,
appId appId
@@ -224,6 +226,7 @@ export const useGetIntegrationAuthRailwayServices = ({
}); });
}; };
export const useDeleteIntegrationAuth = () => { export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient(); 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 }, { workspaceId, env, folderId },
"secrets" "secrets"
], ],
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-imports"
],
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
}; };
+2
View File
@@ -34,6 +34,7 @@ export type DecryptedSecret = {
valueOverride?: string; valueOverride?: string;
idOverride?: string; idOverride?: string;
overrideAction?: string; overrideAction?: string;
folderId?: string;
}; };
export type EncryptedSecretVersion = { export type EncryptedSecretVersion = {
@@ -98,6 +99,7 @@ export type GetProjectSecretsDTO = {
folderId?: string; folderId?: string;
secretPath?: string; secretPath?: string;
isPaused?: boolean; isPaused?: boolean;
include_imports?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void; onSuccess?: (data: DecryptedSecret[]) => void;
}; };
+13
View File
@@ -16,6 +16,7 @@ import {
CreateAPIKeyRes, CreateAPIKeyRes,
DeletOrgMembershipDTO, DeletOrgMembershipDTO,
OrgUser, OrgUser,
RenameUserDTO,
TokenVersion, TokenVersion,
UpdateOrgUserRoleDTO, UpdateOrgUserRoleDTO,
User} from "./types"; User} from "./types";
@@ -45,6 +46,18 @@ const fetchUserAction = async (action: string) => {
return data.userAction; 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) => export const useGetUserAction = (action: string) =>
useQuery({ useQuery({
queryKey: userKeys.userAction, queryKey: userKeys.userAction,
+4
View File
@@ -67,6 +67,10 @@ export type CreateAPIKeyRes = {
apiKeyData: APIKeyData; apiKeyData: APIKeyData;
} }
export type RenameUserDTO = {
newName: string;
};
export type APIKeyData = { export type APIKeyData = {
_id: string; _id: string;
name: string; name: string;
+10 -1
View File
@@ -408,7 +408,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
<a> <a>
<MenuItem <MenuItem
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`} isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
icon="system-outline-168-view-headline" icon="system-outline-168-view-headline"
> >
Audit Logs Audit Logs
@@ -473,6 +472,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem> </MenuItem>
</a> </a>
</Link> </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> <Link href={`/org/${currentOrg?._id}/billing`} passHref>
<a> <a>
<MenuItem <MenuItem
@@ -1,7 +1,7 @@
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
export type GitRisks = { export type GitRisks = {
id: string; _id: string;
description: string; description: string;
startLine: string; startLine: string;
endLine: string; endLine: string;
@@ -20,6 +20,7 @@ export type GitRisks = {
tags: string[]; tags: string[];
ruleID: string; ruleID: string;
fingerprint: string; fingerprint: string;
status: string;
isFalsePositive: boolean; // New field for marking risks as false positives isFalsePositive: boolean; // New field for marking risks as false positives
isResolved: boolean; // New field for marking risks as resolved isResolved: boolean; // New field for marking risks as resolved
@@ -16,7 +16,7 @@ const linkGitAppInstallationWithOrganization = (installationId: string, sessionI
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status === 200) { if (res && res.status === 200) {
return res.json(); return true
} }
console.log("Failed to link installation to organization"); console.log("Failed to link installation to organization");
return undefined; 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> <Head>
<title>{t("common.head-title", { title: t("billing.title") })}</title> <title>{t("common.head-title", { title: t("billing.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head> </Head>
<BillingSettingsPage /> <BillingSettingsPage />
</div> </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>
);
}
@@ -3,6 +3,19 @@ import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { 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, faArrowLeft,
faCheck, faCheck,
faClockRotateLeft, faClockRotateLeft,
@@ -10,12 +23,14 @@ import {
faDownload, faDownload,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFileImport,
faFolderPlus, faFolderPlus,
faMagnifyingGlass, faMagnifyingGlass,
faPlus faPlus
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -41,10 +56,14 @@ import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
import { import {
useBatchSecretsOp, useBatchSecretsOp,
useCreateFolder, useCreateFolder,
useCreateSecretImport,
useCreateWsTag, useCreateWsTag,
useDeleteFolder, useDeleteFolder,
useDeleteSecretImport,
useGetImportedSecrets,
useGetProjectFolders, useGetProjectFolders,
useGetProjectSecrets, useGetProjectSecrets,
useGetSecretImports,
useGetSecretVersion, useGetSecretVersion,
useGetSnapshotSecrets, useGetSnapshotSecrets,
useGetUserAction, useGetUserAction,
@@ -55,7 +74,8 @@ import {
useGetWsTags, useGetWsTags,
usePerformSecretRollback, usePerformSecretRollback,
useRegisterUserAction, useRegisterUserAction,
useUpdateFolder useUpdateFolder,
useUpdateSecretImport
} from "@app/hooks/api"; } from "@app/hooks/api";
import { secretKeys } from "@app/hooks/api/secrets/queries"; import { secretKeys } from "@app/hooks/api/secrets/queries";
import { WorkspaceEnv } from "@app/hooks/api/types"; import { WorkspaceEnv } from "@app/hooks/api/types";
@@ -71,6 +91,8 @@ import {
import { PitDrawer } from "./components/PitDrawer"; import { PitDrawer } from "./components/PitDrawer";
import { SecretDetailDrawer } from "./components/SecretDetailDrawer"; import { SecretDetailDrawer } from "./components/SecretDetailDrawer";
import { SecretDropzone } from "./components/SecretDropzone"; import { SecretDropzone } from "./components/SecretDropzone";
import { SecretImportForm } from "./components/SecretImportForm";
import { SecretImportSection } from "./components/SecretImportSection";
import { SecretInputRow } from "./components/SecretInputRow"; import { SecretInputRow } from "./components/SecretInputRow";
import { SecretTableHeader } from "./components/SecretTableHeader"; import { SecretTableHeader } from "./components/SecretTableHeader";
import { import {
@@ -84,7 +106,7 @@ import {
} from "./DashboardPage.utils"; } from "./DashboardPage.utils";
const USER_ACTION_PUSH = "first_time_secrets_pushed"; 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 * 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 * 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", "compareSecrets",
"folderForm", "folderForm",
"deleteFolder", "deleteFolder",
"upgradePlan" "upgradePlan",
"addSecretImport",
"deleteSecretImport"
] as const); ] as const);
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState(""); const [searchFilter, setSearchFilter] = useState("");
@@ -129,6 +153,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { currentWorkspace, isLoading } = useWorkspace(); const { currentWorkspace, isLoading } = useWorkspace();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const workspaceId = currentWorkspace?._id as string; const workspaceId = currentWorkspace?._id as string;
const selectedEnvSlug = selectedEnv?.slug || "";
const { data: latestFileKey } = useGetUserWsKey(workspaceId); const { data: latestFileKey } = useGetUserWsKey(workspaceId);
useEffect(() => { useEffect(() => {
@@ -161,7 +187,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId, workspaceId,
env: selectedEnv?.slug || "", env: selectedEnvSlug,
decryptFileKey: latestFileKey!, decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId), isPaused: Boolean(snapshotId),
folderId folderId
@@ -169,7 +195,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({ const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
workspaceId: workspaceId || "", workspaceId: workspaceId || "",
environment: selectedEnv?.slug || "", environment: selectedEnvSlug,
parentFolderId: folderId, parentFolderId: folderId,
isPaused: isRollbackMode, isPaused: isRollbackMode,
sortDir sortDir
@@ -182,7 +208,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetchingNextPage isFetchingNextPage
} = useGetWorkspaceSecretSnapshots({ } = useGetWorkspaceSecretSnapshots({
workspaceId, workspaceId,
environment: selectedEnv?.slug || "", environment: selectedEnvSlug,
folder: folderId, folder: folderId,
limit: 10 limit: 10
}); });
@@ -193,17 +219,18 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetching: isSnapshotChanging isFetching: isSnapshotChanging
} = useGetSnapshotSecrets({ } = useGetSnapshotSecrets({
snapshotId: snapshotId || "", snapshotId: snapshotId || "",
env: selectedEnv?.slug || "", env: selectedEnvSlug,
decryptFileKey: latestFileKey! decryptFileKey: latestFileKey!
}); });
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount( const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
workspaceId, workspaceId,
selectedEnv?.slug || "", selectedEnvSlug,
folderId folderId
); );
const { data: wsTags } = useGetWsTags(workspaceId); const { data: wsTags } = useGetWsTags(workspaceId);
// mutation calls // mutation calls
const { mutateAsync: batchSecretOp } = useBatchSecretsOp(); const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
const { mutateAsync: performSecretRollback } = usePerformSecretRollback(); const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
@@ -213,6 +240,50 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { mutateAsync: updateFolder } = useUpdateFolder(folderId); const { mutateAsync: updateFolder } = useUpdateFolder(folderId);
const { mutateAsync: deleteFolder } = useDeleteFolder(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>({ const method = useForm<FormData>({
// why any: well yup inferred ts expects other keys to defined as undefined // why any: well yup inferred ts expects other keys to defined as undefined
defaultValues: secrets as any, defaultValues: secrets as any,
@@ -319,14 +390,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
await performSecretRollback({ await performSecretRollback({
workspaceId, workspaceId,
version: snapshotSecret.version, version: snapshotSecret.version,
environment: selectedEnv?.slug || "", environment: selectedEnvSlug,
folderId folderId
}); });
setValue("isSnapshotMode", false); setValue("isSnapshotMode", false);
setSnaphotId(null); setSnaphotId(null);
queryClient.invalidateQueries( queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug));
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || "")
);
createNotification({ createNotification({
text: "Successfully rollback secrets", text: "Successfully rollback secrets",
type: "success" 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 // when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length; const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
@@ -534,7 +676,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const isSnapshotSecretEmtpy = const isSnapshotSecretEmtpy =
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length; isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy; const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
const isEmptyPage = isFoldersEmpty && isSecretEmpty; const isSecretImportEmpty = !secretImportCfg?.imports?.length;
const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty;
if (isSecretsLoading || isEnvListLoading) { if (isSecretsLoading || isEnvListLoading) {
return ( return (
@@ -652,54 +795,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</Button> </Button>
</div> </div>
{!isReadOnly && !isRollbackMode && ( {!isReadOnly && !isRollbackMode && (
<> <div className="flex flex-row items-center justify-center">
<div className="block lg:hidden"> <button
<Tooltip content="Add Folder"> type="button"
<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={() => { onClick={() => {
if (!(isReadOnly || isRollbackMode)) {
if (secretContainer.current) { if (secretContainer.current) {
secretContainer.current.scroll({ secretContainer.current.scroll({
top: 0, top: 0,
@@ -708,15 +808,48 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
} }
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false }); prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
setSearchFilter(""); setSearchFilter("");
}
}} }}
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} isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg" variant="outline_bg"
className="h-10" className="h-10"
isFullWidth
> >
Add Secret Add Folder
</Button> </Button>
</div> </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 && ( {isRollbackMode && (
<Button <Button
@@ -751,10 +884,27 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
ref={secretContainer} ref={secretContainer}
> >
{!isEmptyPage && ( {!isEmptyPage && (
<DndContext
onDragEnd={handleDragEnd}
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
>
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar"> <TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
<table className="secret-table relative"> <table className="secret-table relative">
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} /> <SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto"> <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}
/>
<FolderSection <FolderSection
onFolderOpen={handleFolderOpen} onFolderOpen={handleFolderOpen}
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })} onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
@@ -794,6 +944,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</tbody> </tbody>
</table> </table>
</TableContainer> </TableContainer>
</DndContext>
)} )}
<PitDrawer <PitDrawer
isDrawerOpen={popUp?.secretSnapshots?.isOpen} isDrawerOpen={popUp?.secretSnapshots?.isOpen}
@@ -881,6 +1032,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
/> />
</ModalContent> </ModalContent>
</Modal> </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 <DeleteActionModal
isOpen={popUp.deleteFolder.isOpen} isOpen={popUp.deleteFolder.isOpen}
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name} deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
@@ -888,6 +1053,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
onDeleteApproved={handleFolderDelete} 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 <Modal
isOpen={popUp?.compareSecrets?.isOpen} isOpen={popUp?.compareSecrets?.isOpen}
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)} onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}
@@ -67,7 +67,7 @@ export const FolderSection = ({
ariaLabel="delete" ariaLabel="delete"
onClick={() => handleFolderDelete(id, name)} onClick={() => handleFolderDelete(id, name)}
> >
<FontAwesomeIcon icon={faXmark} /> <FontAwesomeIcon icon={faXmark} size="lg" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </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"> <div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete"> <Tooltip content="Delete">
<IconButton <IconButton
size="md" size="lg"
variant="plain" variant="plain"
colorSchema="danger" colorSchema="danger"
ariaLabel="delete" ariaLabel="delete"
@@ -92,6 +92,10 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "cloudflare-pages": case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`; link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break; break;
case "codefresh":
link = `${window.location.origin}/integrations/codefresh/authorize`;
break;
default: default:
break; break;
} }
@@ -48,6 +48,7 @@ export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } = const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations(); useGetCloudIntegrations();
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } = const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
useGetWorkspaceAuthorizations( useGetWorkspaceAuthorizations(
workspaceId, workspaceId,
@@ -31,6 +31,8 @@ export const CloudIntegrationSection = ({
const isEmpty = !isLoading && !cloudIntegrations?.length; const isEmpty = !isLoading && !cloudIntegrations?.length;
const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
return ( return (
<div> <div>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl"> <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}`} /> <Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
))} ))}
{!isLoading && {!isLoading &&
cloudIntegrations?.map((cloudIntegration) => ( sortedCloudIntegrations?.map((cloudIntegration) => (
<div <div
onKeyDown={() => null} onKeyDown={() => null}
role="button" role="button"
@@ -12,6 +12,8 @@ type Props = {
export const FrameworkIntegrationSection = ({ frameworks }: Props) => { export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const sortedFrameworks = frameworks.sort((a, b) => a.name.localeCompare(b.name));
return ( return (
<> <>
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl"> <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" className="mx-6 mt-4 grid grid-flow-dense gap-3"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }} style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }}
> >
{frameworks.map((framework) => ( {sortedFrameworks.map((framework) => (
<a <a
key={`framework-integration-${framework.slug}`} key={`framework-integration-${framework.slug}`}
href={framework.docsLink} href={framework.docsLink}
@@ -32,7 +32,6 @@ export const IntegrationsSection = ({
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation" "deleteConfirmation"
] as const); ] as const);
return ( return (
<div className="mb-8"> <div className="mb-8">
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl"> <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 = () => { export const BillingSettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full px-6"> <div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl w-full"> <div className="max-w-7xl px-6 w-full">
<div className="my-6"> <div className="my-6">
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p> <p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
<div /> <div />
@@ -15,7 +15,7 @@ export const ChangeLanguageSection = () => {
}; };
return ( 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"> <p className="text-xl font-semibold text-mineshaft-100 mb-8">
{t("settings.personal.change-language")} {t("settings.personal.change-language")}
</p> </p>
@@ -3,10 +3,12 @@ import { ChangePasswordSection } from "../ChangePasswordSection";
import { EmergencyKitSection } from "../EmergencyKitSection"; import { EmergencyKitSection } from "../EmergencyKitSection";
import { SecuritySection } from "../SecuritySection"; import { SecuritySection } from "../SecuritySection";
import { SessionsSection } from "../SessionsSection"; import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
export const PersonalSecurityTab = () => { export const PersonalSecurityTab = () => {
return ( return (
<div> <div>
<UserNameSection />
<ChangeLanguageSection /> <ChangeLanguageSection />
<SecuritySection /> <SecuritySection />
<SessionsSection /> <SessionsSection />
@@ -12,7 +12,7 @@ const tabs = [
export const PersonalTabGroup = () => { export const PersonalTabGroup = () => {
return ( return (
<Tab.Group> <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) => ( {tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}> <Tab as={Fragment} key={tab.key}>
{({ selected }) => ( {({ selected }) => (

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