mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 18:29:06 +00:00
Merge branch 'Infisical:main' into feat/windmill-integration
This commit is contained in:
@@ -105,36 +105,6 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
|
||||
secret-scanning-git-app:
|
||||
name: Build secret scanning git app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 🏗️ Build secret scanning git app and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: secret-engine
|
||||
tags: |
|
||||
infisical/staging_deployment_secret-scanning-git-app:${{ steps.commit.outputs.short }}
|
||||
infisical/staging_deployment_secret-scanning-git-app:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
gamma-deployment:
|
||||
name: Deploy to gamma
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+1
-7
@@ -10,7 +10,6 @@
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"no-console": 2,
|
||||
"quotes": [
|
||||
"error",
|
||||
@@ -35,11 +34,6 @@
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{
|
||||
"ignoreDeclarationSort": true
|
||||
}
|
||||
]
|
||||
"sort-imports": 1
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ RUN npm ci --only-production
|
||||
|
||||
COPY --from=build /app .
|
||||
|
||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||
&& apk add infisical=0.8.1
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
||||
|
||||
Generated
+4856
-1548
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@
|
||||
"passport": "^0.6.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"posthog-node": "^2.6.0",
|
||||
"probot": "^12.3.1",
|
||||
"query-string": "^7.1.3",
|
||||
"rate-limit-mongo": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
@@ -103,6 +104,7 @@
|
||||
"jest-junit": "^15.0.0",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm": "^8.19.3",
|
||||
"smee-client": "^1.2.3",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1"
|
||||
|
||||
@@ -10,7 +10,7 @@ export const getEncryptionKey = async () => {
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
}
|
||||
export const getRootEncryptionKey = async () => {
|
||||
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
|
||||
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
}
|
||||
export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
|
||||
@@ -57,6 +57,11 @@ export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWOR
|
||||
export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
|
||||
export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
|
||||
|
||||
export const getSecretScanningWebhookProxy = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
|
||||
export const getSecretScanningWebhookSecret = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
|
||||
export const getSecretScanningGitAppId = async () => (await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
|
||||
export const getSecretScanningPrivateKey = async () => (await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
|
||||
|
||||
export const getLicenseKey = async () => {
|
||||
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as userController from "./userController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as secretScanningController from "./secretScanningController";
|
||||
import * as webhookController from "./webhookController";
|
||||
import * as secretImportController from "./secretImportController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
@@ -33,5 +34,6 @@ export {
|
||||
userController,
|
||||
workspaceController,
|
||||
secretScanningController,
|
||||
webhookController
|
||||
webhookController,
|
||||
secretImportController
|
||||
};
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Request, Response } from "express";
|
||||
import { validateMembership } from "../../helpers";
|
||||
import SecretImport from "../../models/secretImports";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { ADMIN, MEMBER } from "../../variables";
|
||||
|
||||
export const createSecretImport = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment, folderId, secretImport } = req.body;
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
if (!importSecDoc) {
|
||||
const doc = new SecretImport({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
|
||||
});
|
||||
await doc.save();
|
||||
return res.status(200).json({ message: "successfully created secret import" });
|
||||
}
|
||||
|
||||
const doesImportExist = importSecDoc.imports.find(
|
||||
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
|
||||
);
|
||||
if (doesImportExist) {
|
||||
throw BadRequestError({ message: "Secret import already exist" });
|
||||
}
|
||||
|
||||
importSecDoc.imports.push({
|
||||
environment: secretImport.environment,
|
||||
secretPath: secretImport.secretPath
|
||||
});
|
||||
await importSecDoc.save();
|
||||
return res.status(200).json({ message: "successfully created secret import" });
|
||||
};
|
||||
|
||||
// to keep the ordering, you must pass all the imports in here not the only updated one
|
||||
// this is because the order decide which import gets overriden
|
||||
export const updateSecretImport = async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const { secretImports } = req.body;
|
||||
const importSecDoc = await SecretImport.findById(id);
|
||||
if (!importSecDoc) {
|
||||
throw BadRequestError({ message: "Import not found" });
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: importSecDoc.workspace,
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
});
|
||||
|
||||
importSecDoc.imports = secretImports;
|
||||
await importSecDoc.save();
|
||||
return res.status(200).json({ message: "successfully updated secret import" });
|
||||
};
|
||||
|
||||
export const deleteSecretImport = async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const { secretImportEnv, secretImportPath } = req.body;
|
||||
const importSecDoc = await SecretImport.findById(id);
|
||||
if (!importSecDoc) {
|
||||
throw BadRequestError({ message: "Import not found" });
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: importSecDoc.workspace,
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
});
|
||||
importSecDoc.imports = importSecDoc.imports.filter(
|
||||
({ environment, secretPath }) =>
|
||||
!(environment === secretImportEnv && secretPath === secretImportPath)
|
||||
);
|
||||
await importSecDoc.save();
|
||||
return res.status(200).json({ message: "successfully delete secret import" });
|
||||
};
|
||||
|
||||
export const getSecretImports = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment, folderId } = req.query;
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
if (!importSecDoc) {
|
||||
return res.status(200).json({ secretImport: {} });
|
||||
}
|
||||
|
||||
return res.status(200).json({ secretImport: importSecDoc });
|
||||
};
|
||||
|
||||
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environment, folderId } = req.query as {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
};
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
if (!importSecDoc) {
|
||||
return res.status(200).json({ secrets: {} });
|
||||
}
|
||||
|
||||
const secrets = await getAllImportedSecrets(workspaceId, environment, folderId);
|
||||
return res.status(200).json({ secrets });
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { Types } from "mongoose";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
|
||||
import { MembershipOrg } from "../../models";
|
||||
import GitRisks, { STATUS_UNRESOLVED } from "../../models/gitRisks";
|
||||
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../models/gitRisks";
|
||||
|
||||
export const createInstallationSession = async (req: Request, res: Response) => {
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
@@ -72,7 +72,7 @@ export const getCurrentOrganizationInstallationStatus = async (req: Request, res
|
||||
|
||||
export const getRisksForOrganization = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params
|
||||
const risks = await GitRisks.find({ organization: organizationId, status: STATUS_UNRESOLVED }).lean()
|
||||
const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean()
|
||||
res.json({
|
||||
risks: risks
|
||||
})
|
||||
@@ -81,9 +81,11 @@ export const getRisksForOrganization = async (req: Request, res: Response) => {
|
||||
export const updateRisksStatus = async (req: Request, res: Response) => {
|
||||
const { riskId } = req.params
|
||||
const { status } = req.body
|
||||
const risks = await GitRisks.findByIdAndUpdate(riskId, {
|
||||
sttaus: status
|
||||
const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false
|
||||
const risk = await GitRisks.findByIdAndUpdate(riskId, {
|
||||
status: status,
|
||||
isResolved: isRiskResolved
|
||||
}).lean()
|
||||
|
||||
res.json(risks)
|
||||
res.json(risk)
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "../../services/FolderService";
|
||||
import { isValidScope } from "../../helpers/secrets";
|
||||
import path from "path";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
|
||||
/**
|
||||
* Peform a batch of any specified CUD secret operations
|
||||
@@ -690,7 +691,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
|
||||
const { tagSlugs, secretPath } = req.query;
|
||||
const { tagSlugs, secretPath, include_imports } = req.query;
|
||||
let { folderId } = req.query;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
@@ -827,6 +828,12 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
secrets = await Secret.find(secretQuery).populate("tags");
|
||||
}
|
||||
|
||||
// TODO(akhilmhdh) - secret-imp change this to org type
|
||||
let importedSecrets: any[] = [];
|
||||
if (include_imports) {
|
||||
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
|
||||
}
|
||||
|
||||
const channel = getChannelFromUserAgent(req.headers["user-agent"]);
|
||||
|
||||
const readAction = await EELogService.createAction({
|
||||
@@ -868,7 +875,8 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
secrets,
|
||||
...(include_imports && { imports: importedSecrets })
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Types } from "mongoose";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import {
|
||||
MembershipOrg,
|
||||
User,
|
||||
APIKeyData,
|
||||
TokenVersion
|
||||
MembershipOrg,
|
||||
TokenVersion,
|
||||
User
|
||||
} from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
|
||||
@@ -80,6 +80,26 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current user's name [firstName, lastName].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateName = async (req: Request, res: Response) => {
|
||||
const { firstName, lastName }: { firstName: string; lastName: string; } = req.body;
|
||||
req.user.firstName = firstName;
|
||||
req.user.lastName = lastName || "";
|
||||
|
||||
await req.user.save();
|
||||
|
||||
const user = req.user;
|
||||
|
||||
return res.status(200).send({
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return organizations that the current user is part of.
|
||||
* @param req
|
||||
|
||||
@@ -5,6 +5,10 @@ import { eventPushSecrets } from "../../events";
|
||||
import { BotService } from "../../services";
|
||||
import { repackageSecretToRaw } from "../../helpers/secrets";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
import Folder from "../../models/folder";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
|
||||
/**
|
||||
* Return secrets for workspace with id [workspaceId] and environment
|
||||
@@ -16,6 +20,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
const includeImports = req.query.include_imports as string;
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
@@ -28,13 +33,38 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (includeImports) {
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
let folderId = "root";
|
||||
// if folder exist get it and replace folderid with new one
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath as string);
|
||||
if (!folder) {
|
||||
throw BadRequestError({ message: "Folder not found" });
|
||||
}
|
||||
folderId = folder.id;
|
||||
}
|
||||
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
|
||||
return res.status(200).send({
|
||||
secrets: secrets.map((secret) =>
|
||||
repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
})
|
||||
),
|
||||
imports: importedSecrets.map((el) => ({
|
||||
...el,
|
||||
secrets: el.secrets.map((secret) => repackageSecretToRaw({ secret, key }))
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: secrets.map((secret) => {
|
||||
const rep = repackageSecretToRaw({
|
||||
secret,
|
||||
key
|
||||
});
|
||||
|
||||
return rep;
|
||||
})
|
||||
});
|
||||
@@ -232,6 +262,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
const includeImports = req.query.include_imports as string;
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
@@ -240,6 +271,24 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
if (includeImports) {
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
let folderId = "root";
|
||||
// if folder exist get it and replace folderid with new one
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath as string);
|
||||
if (!folder) {
|
||||
throw BadRequestError({ message: "Folder not found" });
|
||||
}
|
||||
folderId = folder.id;
|
||||
}
|
||||
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
|
||||
return res.status(200).send({
|
||||
secrets,
|
||||
imports: importedSecrets
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
|
||||
+29
-4
@@ -5,11 +5,12 @@ import express from "express";
|
||||
require("express-async-errors");
|
||||
import helmet from "helmet";
|
||||
import cors from "cors";
|
||||
import { DatabaseService } from "./services";
|
||||
import { DatabaseService, GithubSecretScanningService } from "./services";
|
||||
import { EELicenseService } from "./ee/services";
|
||||
import { setUpHealthEndpoint } from "./services/health";
|
||||
import cookieParser from "cookie-parser";
|
||||
import swaggerUi = require("swagger-ui-express");
|
||||
import { Probot, createNodeMiddleware } from "probot";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const swaggerFile = require("../spec.json");
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@@ -41,13 +42,14 @@ import {
|
||||
userAction as v1UserActionRouter,
|
||||
user as v1UserRouter,
|
||||
workspace as v1WorkspaceRouter,
|
||||
webhooks as v1WebhooksRouter
|
||||
webhooks as v1WebhooksRouter,
|
||||
secretImport as v1SecretImportRouter
|
||||
} from "./routes/v1";
|
||||
import {
|
||||
auth as v2AuthRouter,
|
||||
organizations as v2OrganizationsRouter,
|
||||
signup as v2SignupRouter,
|
||||
users as v2UsersRouter,
|
||||
organizations as v2OrganizationsRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
secret as v2SecretRouter, // begin to phase out
|
||||
secrets as v2SecretsRouter,
|
||||
@@ -66,10 +68,12 @@ import { healthCheck } from "./routes/status";
|
||||
import { getLogger } from "./utils/logger";
|
||||
import { RouteNotFoundError } from "./utils/errors";
|
||||
import { requestErrorHandler } from "./middleware/requestErrorHandler";
|
||||
import { getNodeEnv, getPort, getSiteURL } from "./config";
|
||||
import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config";
|
||||
import { setup } from "./utils/setup";
|
||||
const SmeeClient = require('smee-client') // eslint-disable-line
|
||||
|
||||
const main = async () => {
|
||||
|
||||
await setup();
|
||||
|
||||
await EELicenseService.initGlobalFeatureSet();
|
||||
@@ -85,6 +89,26 @@ const main = async () => {
|
||||
})
|
||||
);
|
||||
|
||||
if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) {
|
||||
const probot = new Probot({
|
||||
appId: await getSecretScanningGitAppId(),
|
||||
privateKey: await getSecretScanningPrivateKey(),
|
||||
secret: await getSecretScanningWebhookSecret(),
|
||||
});
|
||||
|
||||
if ((await getNodeEnv()) != "production") {
|
||||
const smee = new SmeeClient({
|
||||
source: await getSecretScanningWebhookProxy(),
|
||||
target: "http://backend:4000/ss-webhook",
|
||||
logger: console
|
||||
})
|
||||
|
||||
smee.start()
|
||||
}
|
||||
|
||||
app.use(createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })); // secret scanning webhook
|
||||
}
|
||||
|
||||
if ((await getNodeEnv()) === "production") {
|
||||
// enable app-wide rate-limiting + helmet security
|
||||
// in production
|
||||
@@ -128,6 +152,7 @@ const main = async () => {
|
||||
app.use("/api/v1/folders", v1SecretsFolder);
|
||||
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
|
||||
app.use("/api/v1/webhooks", v1WebhooksRouter);
|
||||
app.use("/api/v1/secret-imports", v1SecretImportRouter);
|
||||
|
||||
// v2 routes (improvements)
|
||||
app.use("/api/v2/signup", v2SignupRouter);
|
||||
|
||||
@@ -25,6 +25,7 @@ export type GitRisks = {
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
fingerPrintWithoutCommitId: string
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
@@ -94,6 +95,9 @@ const gitRisks = new Schema<GitRisks>({
|
||||
type: String,
|
||||
unique: true
|
||||
},
|
||||
fingerPrintWithoutCommitId: {
|
||||
type: String,
|
||||
},
|
||||
isFalsePositive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
||||
@@ -16,13 +16,14 @@ import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; //
|
||||
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
|
||||
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
|
||||
import TokenData, { ITokenData } from "./tokenData";
|
||||
import User,{ AuthProvider, IUser } from "./user";
|
||||
import User, { AuthProvider, IUser } from "./user";
|
||||
import UserAction, { IUserAction } from "./userAction";
|
||||
import Workspace, { IWorkspace } from "./workspace";
|
||||
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
|
||||
import APIKeyData, { IAPIKeyData } from "./apiKeyData";
|
||||
import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
|
||||
import TokenVersion, { ITokenVersion } from "./tokenVersion";
|
||||
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./gitRisks";
|
||||
|
||||
export {
|
||||
AuthProvider,
|
||||
@@ -76,4 +77,6 @@ export {
|
||||
ILoginSRPDetail,
|
||||
TokenVersion,
|
||||
ITokenVersion,
|
||||
GitRisks,
|
||||
STATUS_RESOLVED_FALSE_POSITIVE
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -1,5 +1,5 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import { getSmtpConfigured } from "../../config";
|
||||
import { getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -10,6 +10,7 @@ router.get(
|
||||
date: new Date(),
|
||||
message: "Ok",
|
||||
emailConfigured: await getSmtpConfigured(),
|
||||
secretScanningConfigured: await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey(),
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
|
||||
import secretsFolder from "./secretsFolder";
|
||||
import secretScanning from "./secretScanning";
|
||||
import webhooks from "./webhook";
|
||||
import secretImport from "./secretImport";
|
||||
|
||||
export {
|
||||
signup,
|
||||
@@ -37,5 +38,6 @@ export {
|
||||
integrationAuth,
|
||||
secretsFolder,
|
||||
secretScanning,
|
||||
webhooks
|
||||
webhooks,
|
||||
secretImport
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -69,6 +69,7 @@ router.post(
|
||||
}),
|
||||
param("organizationId").exists().trim(),
|
||||
param("riskId").exists().trim(),
|
||||
body("status").exists(),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
|
||||
@@ -127,6 +127,7 @@ router.get(
|
||||
query("tagSlugs"),
|
||||
query("folderId").default("root").isString().trim(),
|
||||
query("secretPath").optional().isString().trim(),
|
||||
query("include_imports").optional().default(false).isBoolean(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
|
||||
@@ -29,6 +29,16 @@ router.patch(
|
||||
usersController.updateMyMfaEnabled
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/me/name",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||
}),
|
||||
body("firstName").exists(),
|
||||
validateRequest,
|
||||
usersController.updateName
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/me/organizations",
|
||||
requireAuth({
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
} from "../../middleware";
|
||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
|
||||
import { body, param, query } from "express-validator";
|
||||
import { secretsController } from "../../controllers/v3";
|
||||
import {
|
||||
@@ -17,7 +13,7 @@ import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../../variables";
|
||||
|
||||
router.get(
|
||||
@@ -25,14 +21,15 @@ router.get(
|
||||
query("workspaceId").exists().isString().trim(),
|
||||
query("environment").exists().isString().trim(),
|
||||
query("secretPath").default("/").isString().trim(),
|
||||
query("include_imports").isBoolean().default(false),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -40,7 +37,7 @@ router.get(
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.getSecretsRaw
|
||||
);
|
||||
@@ -58,8 +55,8 @@ router.get(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -67,7 +64,7 @@ router.get(
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.getSecretByNameRaw
|
||||
);
|
||||
@@ -86,8 +83,8 @@ router.post(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -95,7 +92,7 @@ router.post(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.createSecretRaw
|
||||
);
|
||||
@@ -114,8 +111,8 @@ router.patch(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -123,7 +120,7 @@ router.patch(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.updateSecretByNameRaw
|
||||
);
|
||||
@@ -141,8 +138,8 @@ router.delete(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -150,7 +147,7 @@ router.delete(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.deleteSecretByNameRaw
|
||||
);
|
||||
@@ -166,8 +163,8 @@ router.get(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -175,7 +172,7 @@ router.get(
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
);
|
||||
@@ -201,8 +198,8 @@ router.post(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -210,7 +207,7 @@ router.post(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.createSecret
|
||||
);
|
||||
@@ -228,15 +225,15 @@ router.get(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "query",
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireBlindIndicesEnabled: true
|
||||
}),
|
||||
secretsController.getSecretByName
|
||||
);
|
||||
@@ -257,8 +254,8 @@ router.patch(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -266,7 +263,7 @@ router.patch(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.updateSecretByName
|
||||
);
|
||||
@@ -284,8 +281,8 @@ router.delete(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -293,7 +290,7 @@ router.delete(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.deleteSecretByName
|
||||
);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Probot } from "probot";
|
||||
import { exec } from "child_process";
|
||||
import { writeFile, readFile, rm, mkdir, } from "fs";
|
||||
import { mkdir, readFile, rm, writeFile } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path"
|
||||
import mongoose from "mongoose";
|
||||
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./models/gitRisks";
|
||||
import GitAppOrganizationInstallation from "./models/gitAppOrganizationInstallation";
|
||||
import { sendMail, setTransporter } from "./helper/nodemailer";
|
||||
import { initSmtp } from "./service/smtp";
|
||||
import MembershipOrg, { ADMIN, OWNER } from "./models/membershipOrg";
|
||||
import User from "./models/user";
|
||||
import GitRisks from "../models/gitRisks";
|
||||
import GitAppOrganizationInstallation from "../models/gitAppOrganizationInstallation";
|
||||
import MembershipOrg from "../models/membershipOrg";
|
||||
import { ADMIN, OWNER } from "../variables";
|
||||
import User from "../models/user";
|
||||
import { sendMail } from "../helpers";
|
||||
import TelemetryService from "./TelemetryService";
|
||||
|
||||
type SecretMatch = {
|
||||
Description: string;
|
||||
@@ -30,19 +30,10 @@ type SecretMatch = {
|
||||
Tags: string[];
|
||||
RuleID: string;
|
||||
Fingerprint: string;
|
||||
FingerPrintWithoutCommitId: string
|
||||
};
|
||||
|
||||
export = async (app: Probot) => {
|
||||
// connect to DB
|
||||
initDatabase()
|
||||
|
||||
setTransporter(await initSmtp());
|
||||
|
||||
app.on("installation.created", async (context) => {
|
||||
const { payload } = context;
|
||||
// console.log("payload==>", payload.installation.repository_selection)
|
||||
})
|
||||
|
||||
export default async (app: Probot) => {
|
||||
app.on("installation.deleted", async (context) => {
|
||||
const { payload } = context;
|
||||
const { installation, repositories } = payload;
|
||||
@@ -50,8 +41,10 @@ export = async (app: Probot) => {
|
||||
await GitRisks.deleteMany({ installationId: installation.id })
|
||||
await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id })
|
||||
} else {
|
||||
for (const repository of repositories) {
|
||||
await GitRisks.deleteMany({ repositoryId: repository.id })
|
||||
if (repositories) {
|
||||
for (const repository of repositories) {
|
||||
await GitRisks.deleteMany({ repositoryId: repository.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -59,9 +52,13 @@ export = async (app: Probot) => {
|
||||
app.on("push", async (context) => {
|
||||
const { payload } = context;
|
||||
const { commits, repository, installation, pusher } = payload;
|
||||
const [owner, repo] = repository.full_name.split('/');
|
||||
const [owner, repo] = repository.full_name.split("/");
|
||||
|
||||
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation.id }).lean()
|
||||
if (!commits || !repository || !installation || !pusher) {
|
||||
return
|
||||
}
|
||||
|
||||
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation?.id }).lean()
|
||||
if (!installationLinkToOrgExists) {
|
||||
return
|
||||
}
|
||||
@@ -78,31 +75,30 @@ export = async (app: Probot) => {
|
||||
});
|
||||
|
||||
const data: any = fileContentsResponse.data;
|
||||
const fileContent = Buffer.from(data.content, 'base64').toString();
|
||||
const fileContent = Buffer.from(data.content, "base64").toString();
|
||||
|
||||
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // to count lines correctly
|
||||
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // extra line to count lines correctly
|
||||
|
||||
for (const finding of findings) {
|
||||
const fingerPrint = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
|
||||
finding.Fingerprint = fingerPrint
|
||||
const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
|
||||
const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`
|
||||
finding.Fingerprint = fingerPrintWithCommitId
|
||||
finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId
|
||||
finding.Commit = commit.id
|
||||
finding.File = filepath
|
||||
finding.Author = commit.author.name
|
||||
finding.Email = commit.author.email
|
||||
allFindingsByFingerprint[fingerPrint] = finding
|
||||
finding.Email = commit?.author?.email ? commit?.author?.email : ""
|
||||
|
||||
allFindingsByFingerprint[fingerPrintWithCommitId] = finding
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching content for ${filepath}`, error);
|
||||
console.error(`Error fetching content for ${filepath}`, error); // eslint-disable-line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// change to update
|
||||
const noneFalsePositiveFindings = {}
|
||||
|
||||
for (const key in allFindingsByFingerprint) {
|
||||
const risk = await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint },
|
||||
{
|
||||
@@ -114,11 +110,6 @@ export = async (app: Probot) => {
|
||||
}, {
|
||||
upsert: true
|
||||
}).lean()
|
||||
|
||||
if (risk.status != STATUS_RESOLVED_FALSE_POSITIVE) {
|
||||
noneFalsePositiveFindings[key] = { ...convertKeysToLowercase(allFindingsByFingerprint[key]) }
|
||||
}
|
||||
|
||||
}
|
||||
// get emails of admins
|
||||
const adminsOfWork = await MembershipOrg.find({
|
||||
@@ -137,39 +128,30 @@ export = async (app: Probot) => {
|
||||
|
||||
const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
|
||||
|
||||
const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
|
||||
if (Object.keys(allFindingsByFingerprint).length) {
|
||||
await sendMail({
|
||||
template: "secretLeakIncident.handlebars",
|
||||
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
|
||||
recipients: usersToNotify,
|
||||
substitutions: {
|
||||
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
|
||||
pusher_email: pusher.email,
|
||||
pusher_name: pusher.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
// don't notify if the risk is marked as false positive
|
||||
|
||||
await sendMail({
|
||||
template: "secretLeakIncident.handlebars",
|
||||
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
|
||||
recipients: [pusher.email, ...adminOrOwnerEmails],
|
||||
substitutions: {
|
||||
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
|
||||
pusher_email: pusher.email,
|
||||
pusher_name: pusher.name
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on(['pull_request.opened', 'pull_request.synchronize'], async (context) => {
|
||||
const { payload } = context;
|
||||
const { pull_request } = payload
|
||||
if (false) {
|
||||
const check = {
|
||||
owner: pull_request.head.repo.owner.login,
|
||||
repo: pull_request.head.repo.name,
|
||||
name: 'Secret Detection',
|
||||
head_sha: pull_request.head.sha,
|
||||
status: 'completed',
|
||||
conclusion: 'failure',
|
||||
output: {
|
||||
title: `X Secrets detected`,
|
||||
summary: 'We detected potential leaked secret(s) in your pull request.',
|
||||
},
|
||||
};
|
||||
return context.octokit.checks.create(check);
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "cloud secret scan",
|
||||
distinctId: pusher.email,
|
||||
properties: {
|
||||
numberOfCommitsScanned: commits.length,
|
||||
numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -254,28 +236,13 @@ function deleteTempFolder(folderPath: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
const initDatabase = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGO_URL);
|
||||
// allow empty strings to pass the required validator
|
||||
mongoose.Schema.Types.String.checkRequired(v => typeof v === "string");
|
||||
|
||||
console.log("Database connection established");
|
||||
|
||||
} catch (err) {
|
||||
console.log(`Unable to establish Database connection due to the error.\n${err}`);
|
||||
}
|
||||
|
||||
return mongoose.connection;
|
||||
}
|
||||
|
||||
function convertKeysToLowercase<T>(obj: T): T {
|
||||
const convertedObj = {} as T;
|
||||
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1);
|
||||
convertedObj[lowercaseKey] = obj[key];
|
||||
convertedObj[lowercaseKey as keyof T] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
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;
|
||||
};
|
||||
@@ -6,13 +6,15 @@ import EventService from "./EventService";
|
||||
import IntegrationService from "./IntegrationService";
|
||||
import TokenService from "./TokenService";
|
||||
import SecretService from "./SecretService";
|
||||
import GithubSecretScanningService from "./GithubSecretScanningService"
|
||||
|
||||
export {
|
||||
TelemetryService,
|
||||
DatabaseService,
|
||||
BotService,
|
||||
EventService,
|
||||
IntegrationService,
|
||||
TokenService,
|
||||
SecretService,
|
||||
}
|
||||
TelemetryService,
|
||||
DatabaseService,
|
||||
BotService,
|
||||
EventService,
|
||||
IntegrationService,
|
||||
TokenService,
|
||||
SecretService,
|
||||
GithubSecretScanningService
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Incident alert: secret leaked</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
|
||||
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
|
||||
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
|
||||
by {{pusher_name}} ({{pusher_email}}). If
|
||||
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
|
||||
in the given programming. This will prevent future notifications from being sent out for those secret(s).</p>
|
||||
|
||||
<p>If these are production secrets, please rotate them immediately.</p>
|
||||
|
||||
<p>Once you have taken action, be sure to update the status of the risk in your <a
|
||||
href="https://app.infisical.com/">Infisical
|
||||
dashboard</a>.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -235,6 +235,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
|
||||
SetQueryParam("environment", request.Environment).
|
||||
SetQueryParam("workspaceId", request.WorkspaceId)
|
||||
|
||||
if request.IncludeImport {
|
||||
httpRequest.SetQueryParam("include_imports", "true")
|
||||
}
|
||||
|
||||
if request.SecretPath != "" {
|
||||
httpRequest.SetQueryParam("secretPath", request.SecretPath)
|
||||
}
|
||||
|
||||
+42
-31
@@ -272,40 +272,51 @@ type GetNewAccessTokenWithRefreshTokenResponse struct {
|
||||
}
|
||||
|
||||
type GetEncryptedSecretsV3Request struct {
|
||||
Environment string `json:"environment"`
|
||||
WorkspaceId string `json:"workspaceId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
Environment string `json:"environment"`
|
||||
WorkspaceId string `json:"workspaceId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
IncludeImport bool `json:"include_imports"`
|
||||
}
|
||||
|
||||
type EncryptedSecretV3 struct {
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
} `json:"tags"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
SecretKeyTag string `json:"secretKeyTag"`
|
||||
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||
SecretValueIV string `json:"secretValueIV"`
|
||||
SecretValueTag string `json:"secretValueTag"`
|
||||
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
|
||||
SecretCommentIV string `json:"secretCommentIV"`
|
||||
SecretCommentTag string `json:"secretCommentTag"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
KeyEncoding string `json:"keyEncoding"`
|
||||
Folder string `json:"folder"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ImportedSecretV3 struct {
|
||||
Environment string `json:"environment"`
|
||||
FolderId string `json:"folderId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
Secrets []EncryptedSecretV3 `json:"secrets"`
|
||||
}
|
||||
|
||||
type GetEncryptedSecretsV3Response struct {
|
||||
Secrets []struct {
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
} `json:"tags"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
SecretKeyTag string `json:"secretKeyTag"`
|
||||
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||
SecretValueIV string `json:"secretValueIV"`
|
||||
SecretValueTag string `json:"secretValueTag"`
|
||||
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
|
||||
SecretCommentIV string `json:"secretCommentIV"`
|
||||
SecretCommentTag string `json:"secretCommentTag"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
KeyEncoding string `json:"keyEncoding"`
|
||||
Folder string `json:"folder"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
} `json:"secrets"`
|
||||
Secrets []EncryptedSecretV3 `json:"secrets"`
|
||||
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
|
||||
}
|
||||
|
||||
type CreateSecretV3Request struct {
|
||||
|
||||
@@ -87,7 +87,12 @@ var runCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
|
||||
includeImports, err := cmd.Flags().GetBool("include-imports")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||
@@ -186,6 +191,7 @@ func init() {
|
||||
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
||||
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
|
||||
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
|
||||
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")
|
||||
|
||||
@@ -54,12 +54,17 @@ var secretsCmd = &cobra.Command{
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
includeImports, err := cmd.Flags().GetBool("include-imports")
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
tagSlugs, err := cmd.Flags().GetString("tags")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath})
|
||||
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
@@ -647,6 +652,7 @@ func init() {
|
||||
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
|
||||
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
|
||||
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
|
||||
secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
|
||||
rootCmd.AddCommand(secretsCmd)
|
||||
|
||||
@@ -65,4 +65,5 @@ type GetAllSecretsParameters struct {
|
||||
TagSlugs string
|
||||
WorkspaceId string
|
||||
SecretsPath string
|
||||
IncludeImport bool
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
|
||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
|
||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||
if len(serviceTokenParts) < 4 {
|
||||
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||
@@ -45,9 +45,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
|
||||
}
|
||||
|
||||
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
||||
WorkspaceId: serviceTokenDetails.Workspace,
|
||||
Environment: environment,
|
||||
SecretPath: secretPath,
|
||||
WorkspaceId: serviceTokenDetails.Workspace,
|
||||
Environment: environment,
|
||||
SecretPath: secretPath,
|
||||
IncludeImport: includeImports,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -64,15 +65,22 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
|
||||
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt the required workspace key")
|
||||
}
|
||||
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
|
||||
if err != nil {
|
||||
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
|
||||
}
|
||||
|
||||
if includeImports {
|
||||
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
|
||||
if err != nil {
|
||||
return nil, api.GetServiceTokenDetailsResponse{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return plainTextSecrets, serviceTokenDetails, nil
|
||||
}
|
||||
|
||||
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
|
||||
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
@@ -114,8 +122,9 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
|
||||
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
|
||||
|
||||
getSecretsRequest := api.GetEncryptedSecretsV3Request{
|
||||
WorkspaceId: workspaceId,
|
||||
Environment: environmentName,
|
||||
WorkspaceId: workspaceId,
|
||||
Environment: environmentName,
|
||||
IncludeImport: includeImports,
|
||||
// TagSlugs: tagSlugs,
|
||||
}
|
||||
|
||||
@@ -124,19 +133,53 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
|
||||
}
|
||||
|
||||
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
|
||||
}
|
||||
|
||||
if includeImports {
|
||||
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return plainTextSecrets, nil
|
||||
}
|
||||
|
||||
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
|
||||
if importedSecrets == nil {
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
hasOverriden := make(map[string]bool)
|
||||
for _, sec := range secrets {
|
||||
hasOverriden[sec.Key] = true
|
||||
}
|
||||
|
||||
for i := len(importedSecrets) - 1; i >= 0; i-- {
|
||||
importSec := importedSecrets[i]
|
||||
plainTextImportedSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, importSec.Secrets)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decrypt your imported secrets [err=%v]", err)
|
||||
}
|
||||
|
||||
for _, sec := range plainTextImportedSecrets {
|
||||
if _, ok := hasOverriden[sec.Key]; !ok {
|
||||
secrets = append(secrets, sec)
|
||||
hasOverriden[sec.Key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
|
||||
var infisicalToken string
|
||||
if params.InfisicalToken == "" {
|
||||
@@ -179,7 +222,8 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
|
||||
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
|
||||
}
|
||||
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath)
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId,
|
||||
params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport)
|
||||
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
|
||||
|
||||
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
|
||||
@@ -199,7 +243,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
|
||||
|
||||
} else {
|
||||
log.Debug().Msg("Trying to fetch secrets using service token")
|
||||
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath)
|
||||
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath, params.IncludeImport)
|
||||
}
|
||||
|
||||
return secretsToReturn, errorToReturn
|
||||
@@ -427,9 +471,9 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
|
||||
return secretsToReturn
|
||||
}
|
||||
|
||||
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) {
|
||||
func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) ([]models.SingleEnvironmentVariable, error) {
|
||||
plainTextSecrets := []models.SingleEnvironmentVariable{}
|
||||
for _, secret := range encryptedSecrets.Secrets {
|
||||
for _, secret := range encryptedSecrets {
|
||||
// Decrypt key
|
||||
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
|
||||
if err != nil {
|
||||
|
||||
@@ -58,31 +58,6 @@ services:
|
||||
networks:
|
||||
- infisical-dev
|
||||
|
||||
# git-app:
|
||||
# container_name: infisical-dev-git-app
|
||||
# restart: unless-stopped
|
||||
# depends_on:
|
||||
# - mongo
|
||||
# - smtp-server
|
||||
# - backend
|
||||
# - frontend
|
||||
# volumes:
|
||||
# - ./secret-engine/src:/app/src/ # mounted whole src to avoid missing reload on new files
|
||||
# ports:
|
||||
# - "3000:3001"
|
||||
# build:
|
||||
# context: ./secret-engine
|
||||
# dockerfile: Dockerfile.dev
|
||||
# command: npm run start
|
||||
# env_file: ./secret-engine/.env
|
||||
# environment:
|
||||
# - NODE_ENV=development
|
||||
# - MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
||||
# networks:
|
||||
# - infisical-dev
|
||||
# extra_hosts:
|
||||
# - "host.docker.internal:host-gateway"
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
container_name: infisical-dev-mongo
|
||||
|
||||
@@ -6,7 +6,7 @@ The changelog below reflects new product developments and updates on a monthly b
|
||||
|
||||
## July 2023
|
||||
|
||||
- Released [secret referencing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments.
|
||||
- Released [secret referencing and importing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments.
|
||||
- Added the [intergation with Laravel Forge](https://infisical.com/docs/integrations/cloud/laravel-forge).
|
||||
- Redesigned the project/organization experience.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Reference Secrets"
|
||||
title: "Reference/Import Secrets"
|
||||
description: "How to use reference secrets in Infisical"
|
||||
---
|
||||
|
||||
@@ -29,9 +29,24 @@ Here are a few more examples to help you understand how to reference secrets in
|
||||
## Fetching fully constructed values
|
||||
|
||||
Secret referencing combines multiple secrets into one unified value, reconstructed only on the client side. To retrieve this value, you need access to read the environment and [folder](./folder) from where the secrets originate.
|
||||
For instance, to access a secret 'A' composed of secrets 'B' and 'C' from different environments, you must have read access to both.
|
||||
For instance, to access a secret 'A' composed of secrets 'B' and 'C' from different environments, you must have read access to both 'A' and 'B'
|
||||
|
||||
When using [service tokens](./token) to fetch referenced secrets, ensure the service token has read access to all referenced environments and folders.
|
||||
Without proper permissions, the final secret value may be incomplete.
|
||||
|
||||
## Import entire folders
|
||||
|
||||
While secret referencing effectively minimizes duplication, there might be instances where you need to import or replicate an entire folder's secrets into another. This can be achieved using the 'Import' feature.
|
||||
|
||||
This feature allows you to link secrets from one environment/folder into another environment/folder. It proves beneficial when you have common secrets that need to be available across multiple environments/folders.
|
||||
|
||||
To add an import, simply click on the `Add import` button and provide the environment and secret path from where the secrets should be imported.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@@ -12,7 +12,7 @@ This level of control not only ensures maximum flexibility but also significantl
|
||||
|
||||
## Creating a service token
|
||||
|
||||
To generate the the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access.
|
||||
To generate the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access.
|
||||
|
||||

|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 288 KiB |
@@ -8,7 +8,7 @@ Self-hosted Infisical allows you to maintain your sensitive information within y
|
||||
Choose from a variety of deployment options listed below to get started.
|
||||
|
||||
<Card
|
||||
title="Kubernetes"
|
||||
title="Kubernetes (recommended)"
|
||||
color="#ea5a0c"
|
||||
href="deployment-options/kubernetes-helm"
|
||||
>
|
||||
|
||||
Generated
+109
@@ -5,6 +5,9 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
@@ -2468,6 +2471,68 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
|
||||
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
|
||||
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/modifiers": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
|
||||
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.0.6",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
|
||||
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.0.7",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
|
||||
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
||||
@@ -24491,6 +24556,50 @@
|
||||
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
|
||||
"dev": true
|
||||
},
|
||||
"@dnd-kit/accessibility": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
|
||||
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@dnd-kit/core": {
|
||||
"version": "6.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
|
||||
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
|
||||
"requires": {
|
||||
"@dnd-kit/accessibility": "^3.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@dnd-kit/modifiers": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
|
||||
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
|
||||
"requires": {
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@dnd-kit/sortable": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
|
||||
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
|
||||
"requires": {
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@dnd-kit/utilities": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
|
||||
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@emotion/babel-plugin": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@ export const EmptyState = ({
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} />
|
||||
<div className="flex flex-row items-center py-4">
|
||||
<div className="text-sm text-bunker-300">{title}</div>
|
||||
<div>{children}</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./secretFolders";
|
||||
export * from "./secretImports";
|
||||
export * from "./secrets";
|
||||
export * from "./secretSnapshots";
|
||||
export * from "./serviceAccounts";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
|
||||
export { useGetImportedSecrets, useGetSecretImports } from "./queries";
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretImportKeys } from "./queries";
|
||||
import { TCreateSecretImportDTO, TDeleteSecretImportDTO, TUpdateSecretImportDTO } from "./types";
|
||||
|
||||
export const useCreateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateSecretImportDTO>({
|
||||
mutationFn: async ({ secretImport, environment, workspaceId, folderId }) => {
|
||||
const { data } = await apiRequest.post("/api/v1/secret-imports", {
|
||||
secretImport,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateSecretImportDTO>({
|
||||
mutationFn: async ({ environment, workspaceId, folderId, secretImports, id }) => {
|
||||
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
|
||||
secretImports,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretImportDTO>({
|
||||
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
|
||||
data: {
|
||||
secretImportPath,
|
||||
secretImportEnv
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGetImportedSecrets, TImportedSecrets, TSecretImports } from "./types";
|
||||
|
||||
export const secretImportKeys = {
|
||||
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-imports"
|
||||
],
|
||||
getSecretImportSecrets: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-import-sec"
|
||||
]
|
||||
};
|
||||
|
||||
const fetchSecretImport = async (workspaceId: string, environment: string, folderId?: string) => {
|
||||
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
|
||||
"/api/v1/secret-imports",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secretImport;
|
||||
};
|
||||
|
||||
export const useGetSecretImports = (workspaceId: string, env: string, folderId?: string) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId) && Boolean(env),
|
||||
queryKey: secretImportKeys.getProjectSecretImports(workspaceId, env, folderId),
|
||||
queryFn: () => fetchSecretImport(workspaceId, env, folderId)
|
||||
});
|
||||
|
||||
const fetchImportedSecrets = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
folderId?: string
|
||||
) => {
|
||||
const { data } = await apiRequest.get<{ secrets: TImportedSecrets }>(
|
||||
"/api/v1/secret-imports/secrets",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secrets;
|
||||
};
|
||||
|
||||
export const useGetImportedSecrets = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
decryptFileKey
|
||||
}: TGetImportedSecrets) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && Boolean(decryptFileKey),
|
||||
queryKey: secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId),
|
||||
queryFn: () => fetchImportedSecrets(workspaceId, environment, folderId),
|
||||
select: useCallback(
|
||||
(data: TImportedSecrets) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
return data.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath,
|
||||
folderId: el.folderId,
|
||||
secrets: el.secrets.map((encSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
|
||||
return {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
})
|
||||
}));
|
||||
},
|
||||
[decryptFileKey]
|
||||
)
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import { UserWsKeyPair } from "../types";
|
||||
|
||||
export type TSecretImports = {
|
||||
_id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
imports: Array<{ environment: string; secretPath: string }>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TImportedSecrets = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
folderId: string;
|
||||
secrets: EncryptedSecret[];
|
||||
}[];
|
||||
|
||||
export type TGetImportedSecrets = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export type TCreateSecretImportDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImport: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImports: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TDeleteSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImportPath: string;
|
||||
secretImportEnv: string;
|
||||
};
|
||||
@@ -24,6 +24,10 @@ export const secretKeys = {
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets"
|
||||
],
|
||||
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-imports"
|
||||
],
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export type DecryptedSecret = {
|
||||
valueOverride?: string;
|
||||
idOverride?: string;
|
||||
overrideAction?: string;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export type EncryptedSecretVersion = {
|
||||
@@ -98,6 +99,7 @@ export type GetProjectSecretsDTO = {
|
||||
folderId?: string;
|
||||
secretPath?: string;
|
||||
isPaused?: boolean;
|
||||
include_imports?: boolean;
|
||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CreateAPIKeyRes,
|
||||
DeletOrgMembershipDTO,
|
||||
OrgUser,
|
||||
RenameUserDTO,
|
||||
TokenVersion,
|
||||
UpdateOrgUserRoleDTO,
|
||||
User} from "./types";
|
||||
@@ -45,6 +46,18 @@ const fetchUserAction = async (action: string) => {
|
||||
return data.userAction;
|
||||
};
|
||||
|
||||
export const useRenameUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, RenameUserDTO>({
|
||||
mutationFn: ({ newName }) =>
|
||||
apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(userKeys.getUser);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserAction = (action: string) =>
|
||||
useQuery({
|
||||
queryKey: userKeys.userAction,
|
||||
|
||||
@@ -67,6 +67,10 @@ export type CreateAPIKeyRes = {
|
||||
apiKeyData: APIKeyData;
|
||||
}
|
||||
|
||||
export type RenameUserDTO = {
|
||||
newName: string;
|
||||
};
|
||||
|
||||
export type APIKeyData = {
|
||||
_id: string;
|
||||
name: string;
|
||||
|
||||
@@ -408,7 +408,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
@@ -473,6 +472,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?._id}/secret-scanning`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/org/${currentOrg?._id}/secret-scanning`}
|
||||
icon="system-outline-69-document-scan"
|
||||
>
|
||||
Secret Scanning
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?._id}/billing`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export type GitRisks = {
|
||||
id: string;
|
||||
_id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
endLine: string;
|
||||
@@ -20,6 +20,7 @@ export type GitRisks = {
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
status: string;
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
|
||||
@@ -16,7 +16,7 @@ const linkGitAppInstallationWithOrganization = (installationId: string, sessionI
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
return true
|
||||
}
|
||||
console.log("Failed to link installation to organization");
|
||||
return undefined;
|
||||
|
||||
@@ -11,6 +11,7 @@ export default function SettingsBilling() {
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<BillingSettingsPage />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
||||
|
||||
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
|
||||
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
|
||||
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter()
|
||||
const queryParams = router.query
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
||||
|
||||
useEffect(()=>{
|
||||
const linkInstallation = async () => {
|
||||
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
|
||||
try {
|
||||
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
|
||||
if (isLinked){
|
||||
router.reload()
|
||||
}
|
||||
|
||||
console.log("installation verification complete")
|
||||
}catch (e){
|
||||
console.log("app installation is stale, start new session", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetchInstallationStatus = async () => {
|
||||
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
||||
setIntegrationStatus(status)
|
||||
}
|
||||
|
||||
fetchInstallationStatus()
|
||||
linkInstallation()
|
||||
},[queryParams.state, queryParams.installation_id])
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
||||
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
|
||||
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div>
|
||||
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div>
|
||||
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div>
|
||||
</div>
|
||||
{integrationEnabled ? (
|
||||
<div>
|
||||
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/>
|
||||
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/>
|
||||
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center h-[3.25rem]">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="primary"
|
||||
onClick={generateNewIntegrationSession}
|
||||
className="py-2 h-min"
|
||||
>
|
||||
Integrate with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SecretScanningLogsTable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SecretScanning.requireAuth = true;
|
||||
@@ -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 { useRouter } from "next/router";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors
|
||||
} from "@dnd-kit/core";
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
import {
|
||||
faAngleDown,
|
||||
faArrowLeft,
|
||||
faCheck,
|
||||
faClockRotateLeft,
|
||||
@@ -10,12 +23,14 @@ import {
|
||||
faDownload,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileImport,
|
||||
faFolderPlus,
|
||||
faMagnifyingGlass,
|
||||
faPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
@@ -41,10 +56,14 @@ import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useBatchSecretsOp,
|
||||
useCreateFolder,
|
||||
useCreateSecretImport,
|
||||
useCreateWsTag,
|
||||
useDeleteFolder,
|
||||
useDeleteSecretImport,
|
||||
useGetImportedSecrets,
|
||||
useGetProjectFolders,
|
||||
useGetProjectSecrets,
|
||||
useGetSecretImports,
|
||||
useGetSecretVersion,
|
||||
useGetSnapshotSecrets,
|
||||
useGetUserAction,
|
||||
@@ -55,7 +74,8 @@ import {
|
||||
useGetWsTags,
|
||||
usePerformSecretRollback,
|
||||
useRegisterUserAction,
|
||||
useUpdateFolder
|
||||
useUpdateFolder,
|
||||
useUpdateSecretImport
|
||||
} from "@app/hooks/api";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
@@ -71,6 +91,8 @@ import {
|
||||
import { PitDrawer } from "./components/PitDrawer";
|
||||
import { SecretDetailDrawer } from "./components/SecretDetailDrawer";
|
||||
import { SecretDropzone } from "./components/SecretDropzone";
|
||||
import { SecretImportForm } from "./components/SecretImportForm";
|
||||
import { SecretImportSection } from "./components/SecretImportSection";
|
||||
import { SecretInputRow } from "./components/SecretInputRow";
|
||||
import { SecretTableHeader } from "./components/SecretTableHeader";
|
||||
import {
|
||||
@@ -84,7 +106,7 @@ import {
|
||||
} from "./DashboardPage.utils";
|
||||
|
||||
const USER_ACTION_PUSH = "first_time_secrets_pushed";
|
||||
|
||||
type TDeleteSecretImport = { environment: string; secretPath: string };
|
||||
/*
|
||||
* Some imp aspects to consider. Here there are multiple stats changing
|
||||
* Thus ideally we need to use a context. But instead we rely on react hook form
|
||||
@@ -113,7 +135,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
"compareSecrets",
|
||||
"folderForm",
|
||||
"deleteFolder",
|
||||
"upgradePlan"
|
||||
"upgradePlan",
|
||||
"addSecretImport",
|
||||
"deleteSecretImport"
|
||||
] as const);
|
||||
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
@@ -129,6 +153,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const workspaceId = currentWorkspace?._id as string;
|
||||
const selectedEnvSlug = selectedEnv?.slug || "";
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -161,7 +187,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
env: selectedEnv?.slug || "",
|
||||
env: selectedEnvSlug,
|
||||
decryptFileKey: latestFileKey!,
|
||||
isPaused: Boolean(snapshotId),
|
||||
folderId
|
||||
@@ -169,7 +195,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
|
||||
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
|
||||
workspaceId: workspaceId || "",
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
parentFolderId: folderId,
|
||||
isPaused: isRollbackMode,
|
||||
sortDir
|
||||
@@ -182,7 +208,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
isFetchingNextPage
|
||||
} = useGetWorkspaceSecretSnapshots({
|
||||
workspaceId,
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
folder: folderId,
|
||||
limit: 10
|
||||
});
|
||||
@@ -193,17 +219,18 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
isFetching: isSnapshotChanging
|
||||
} = useGetSnapshotSecrets({
|
||||
snapshotId: snapshotId || "",
|
||||
env: selectedEnv?.slug || "",
|
||||
env: selectedEnvSlug,
|
||||
decryptFileKey: latestFileKey!
|
||||
});
|
||||
|
||||
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
|
||||
workspaceId,
|
||||
selectedEnv?.slug || "",
|
||||
selectedEnvSlug,
|
||||
folderId
|
||||
);
|
||||
|
||||
const { data: wsTags } = useGetWsTags(workspaceId);
|
||||
|
||||
// mutation calls
|
||||
const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
|
||||
const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
|
||||
@@ -213,6 +240,50 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const { mutateAsync: updateFolder } = useUpdateFolder(folderId);
|
||||
const { mutateAsync: deleteFolder } = useDeleteFolder(folderId);
|
||||
|
||||
const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports(
|
||||
workspaceId,
|
||||
selectedEnvSlug,
|
||||
folderId
|
||||
);
|
||||
|
||||
const { data: importedSecrets } = useGetImportedSecrets({
|
||||
workspaceId,
|
||||
decryptFileKey: latestFileKey!,
|
||||
environment: selectedEnvSlug,
|
||||
folderId
|
||||
});
|
||||
|
||||
// This is for dnd-kit. As react-query state mutation async
|
||||
// This will act as a placeholder to avoid a glitching animation on dropping items
|
||||
const [items, setItems] = useState<
|
||||
Array<{ environment: string; secretPath: string; id: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isSecretImportCfgFetching ||
|
||||
// case in which u go to a folder and come back to fill in with cache data
|
||||
(items.length === 0 && secretImportCfg?.imports?.length !== 0 && isSecretImportCfgFetching)
|
||||
) {
|
||||
setItems(
|
||||
secretImportCfg?.imports?.map((el) => ({
|
||||
...el,
|
||||
id: `${el.environment}-${el.secretPath}`
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
}, [isSecretImportCfgFetching]);
|
||||
|
||||
const { mutateAsync: createSecretImport } = useCreateSecretImport();
|
||||
const { mutate: updateSecretImportSync } = useUpdateSecretImport();
|
||||
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
useSensor(TouchSensor, {}),
|
||||
useSensor(KeyboardSensor, {})
|
||||
);
|
||||
|
||||
const method = useForm<FormData>({
|
||||
// why any: well yup inferred ts expects other keys to defined as undefined
|
||||
defaultValues: secrets as any,
|
||||
@@ -319,14 +390,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
await performSecretRollback({
|
||||
workspaceId,
|
||||
version: snapshotSecret.version,
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
folderId
|
||||
});
|
||||
setValue("isSnapshotMode", false);
|
||||
setSnaphotId(null);
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || "")
|
||||
);
|
||||
queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug));
|
||||
createNotification({
|
||||
text: "Successfully rollback secrets",
|
||||
type: "success"
|
||||
@@ -522,6 +591,79 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretImportCreate = async (env: string, secretPath: string) => {
|
||||
try {
|
||||
await createSecretImport({
|
||||
workspaceId,
|
||||
environment: selectedEnv?.slug || "",
|
||||
folderId,
|
||||
secretImport: {
|
||||
environment: env,
|
||||
secretPath
|
||||
}
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully create secret link"
|
||||
});
|
||||
handlePopUpClose("addSecretImport");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create secret link",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretImportDelete = async () => {
|
||||
const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport
|
||||
?.data as TDeleteSecretImport;
|
||||
try {
|
||||
if (secretImportCfg?._id) {
|
||||
await deleteSecretImport({
|
||||
workspaceId,
|
||||
environment: selectedEnvSlug,
|
||||
folderId,
|
||||
id: secretImportCfg?._id,
|
||||
secretImportEnv: importEnv,
|
||||
secretImportPath: impSecPath
|
||||
});
|
||||
handlePopUpClose("deleteSecretImport");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully removed secret link"
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to remove secret link",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (evt: DragEndEvent) => {
|
||||
const { active, over } = evt;
|
||||
if (over?.id && active.id !== over.id) {
|
||||
const oldIndex = items.findIndex(({ id }) => id === active.id);
|
||||
const newIndex = items.findIndex(({ id }) => id === over.id);
|
||||
const newImportOrder = arrayMove(items, oldIndex, newIndex);
|
||||
setItems(newImportOrder);
|
||||
updateSecretImportSync({
|
||||
workspaceId,
|
||||
environment: selectedEnvSlug,
|
||||
folderId,
|
||||
id: secretImportCfg?._id || "",
|
||||
secretImports: newImportOrder.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath
|
||||
}))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// when secrets is not loading and secrets list is empty
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
|
||||
|
||||
@@ -534,7 +676,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const isSnapshotSecretEmtpy =
|
||||
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
|
||||
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
|
||||
const isEmptyPage = isFoldersEmpty && isSecretEmpty;
|
||||
const isSecretImportEmpty = !importedSecrets?.length;
|
||||
const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty;
|
||||
|
||||
if (isSecretsLoading || isEnvListLoading) {
|
||||
return (
|
||||
@@ -652,54 +795,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
</Button>
|
||||
</div>
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<>
|
||||
<div className="block lg:hidden">
|
||||
<Tooltip content="Add Folder">
|
||||
<IconButton
|
||||
ariaLabel="recovery"
|
||||
variant="outline_bg"
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFolderPlus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="block lg:hidden">
|
||||
<Tooltip content="Point-in-time Recovery">
|
||||
<IconButton
|
||||
ariaLabel="recovery"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (secretContainer.current) {
|
||||
secretContainer.current.scroll({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!(isReadOnly || isRollbackMode)) {
|
||||
if (secretContainer.current) {
|
||||
secretContainer.current.scroll({
|
||||
top: 0,
|
||||
@@ -708,15 +808,48 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
}
|
||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||
setSearchFilter("");
|
||||
}}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
>
|
||||
Add Secret
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}}
|
||||
className="font-semibold bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-l-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 pr-4 duration-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="px-2"/>Add Secret
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||
<div className="bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-r-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 duration-200">
|
||||
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5"/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="mt-1 z-[60] left-20 w-[10.8rem]">
|
||||
<div className="bg-mineshaft-800 p-1 border border-mineshaft-600 rounded-md">
|
||||
<div className="w-full pb-1">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||
onClick={() => handlePopUpOpen("addSecretImport")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Import
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
{isRollbackMode && (
|
||||
<Button
|
||||
@@ -751,49 +884,67 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
ref={secretContainer}
|
||||
>
|
||||
{!isEmptyPage && (
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-96 overflow-y-auto">
|
||||
<FolderSection
|
||||
onFolderOpen={handleFolderOpen}
|
||||
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
|
||||
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
folders={folderList}
|
||||
search={searchFilter}
|
||||
/>
|
||||
{fields.map(({ id, _id }, index) => (
|
||||
<SecretInputRow
|
||||
key={id}
|
||||
isReadOnly={isReadOnly}
|
||||
isRollbackMode={isRollbackMode}
|
||||
isAddOnly={isAddOnly}
|
||||
index={index}
|
||||
searchTerm={searchFilter}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
wsTags={wsTags}
|
||||
onCreateTagOpen={() => handlePopUpOpen("addTag")}
|
||||
<DndContext
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
>
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-96 overflow-y-auto">
|
||||
<SecretImportSection
|
||||
onSecretImportDelete={(impSecEnv, impSecPath) =>
|
||||
handlePopUpOpen("deleteSecretImport", {
|
||||
environment: impSecEnv,
|
||||
secretPath: impSecPath
|
||||
})
|
||||
}
|
||||
secrets={secrets?.secrets}
|
||||
importedSecrets={importedSecrets}
|
||||
items={items}
|
||||
/>
|
||||
))}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<tr>
|
||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||
onClick={onAppendSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
<span className="ml-2 w-20">Add Secret</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
<FolderSection
|
||||
onFolderOpen={handleFolderOpen}
|
||||
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
|
||||
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
folders={folderList}
|
||||
search={searchFilter}
|
||||
/>
|
||||
{fields.map(({ id, _id }, index) => (
|
||||
<SecretInputRow
|
||||
key={id}
|
||||
isReadOnly={isReadOnly}
|
||||
isRollbackMode={isRollbackMode}
|
||||
isAddOnly={isAddOnly}
|
||||
index={index}
|
||||
searchTerm={searchFilter}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
wsTags={wsTags}
|
||||
onCreateTagOpen={() => handlePopUpOpen("addTag")}
|
||||
/>
|
||||
))}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<tr>
|
||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||
onClick={onAppendSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
<span className="ml-2 w-20">Add Secret</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
</DndContext>
|
||||
)}
|
||||
<PitDrawer
|
||||
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
||||
@@ -881,6 +1032,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={popUp?.addSecretImport?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add Secret Link"
|
||||
subTitle="To inherit secrets from another environment or folder"
|
||||
>
|
||||
<SecretImportForm
|
||||
environments={currentWorkspace?.environments}
|
||||
onCreate={handleSecretImportCreate}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteFolder.isOpen}
|
||||
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
|
||||
@@ -888,6 +1053,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||
onDeleteApproved={handleFolderDelete}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecretImport.isOpen}
|
||||
deleteKey="unlink"
|
||||
title="Do you want to remove this secret import?"
|
||||
subTitle={`This will unlink secrets from environment ${
|
||||
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
||||
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||
onDeleteApproved={handleSecretImportDelete}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp?.compareSecrets?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const FolderSection = ({
|
||||
ariaLabel="delete"
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
onCreate: (environment: string, secretPath: string) => Promise<void>;
|
||||
environments?: Array<{ slug: string; name: string }>;
|
||||
};
|
||||
|
||||
const formSchema = yup.object({
|
||||
environment: yup.string().required().label("Environment").trim(),
|
||||
secretPath: yup
|
||||
.string()
|
||||
.required()
|
||||
.label("Secret Path")
|
||||
.trim()
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
)
|
||||
});
|
||||
|
||||
type TFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const SecretImportForm = ({ onCreate, environments = [] }: Props): JSX.Element => {
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TFormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async ({ environment, secretPath }: TFormData) => {
|
||||
await onCreate(environment, secretPath);
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretImportForm } from "./SecretImportForm";
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { faFileImport, faFolder, faUpDown, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { 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.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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
|
||||
|
||||
import { SecretImportItem } from "./SecretImportItem";
|
||||
|
||||
type TImportedSecrets = Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
folderId: string;
|
||||
secrets: DecryptedSecret[];
|
||||
}>;
|
||||
|
||||
const SECRET_IN_DASHBOARD = "Present In Dashboard";
|
||||
|
||||
export const computeImportedSecretRows = (
|
||||
importedSecEnv: string,
|
||||
importedSecPath: string,
|
||||
importSecrets: TImportedSecrets = [],
|
||||
secrets: DecryptedSecret[] = [],
|
||||
environments: { name: string; slug: string }[] = []
|
||||
) => {
|
||||
const importedSecIndex = importSecrets.findIndex(
|
||||
({ secretPath, environment }) =>
|
||||
secretPath === importedSecPath && importedSecEnv === environment
|
||||
);
|
||||
if (importedSecIndex === -1) return [];
|
||||
|
||||
const importedSec = importSecrets[importedSecIndex];
|
||||
|
||||
const overridenSec: Record<string, { env: string; secretPath: string }> = {};
|
||||
const envSlug2Name: Record<string, string> = {};
|
||||
environments.forEach((el) => {
|
||||
envSlug2Name[el.slug] = el.name;
|
||||
});
|
||||
|
||||
for (let i = importedSecIndex + 1; i < importSecrets.length; i += 1) {
|
||||
importSecrets[i].secrets.forEach((el) => {
|
||||
overridenSec[el.key] = {
|
||||
env: envSlug2Name?.[importSecrets[i].environment] || "unknown",
|
||||
secretPath: importSecrets[i].secretPath
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
secrets.forEach((el) => {
|
||||
overridenSec[el.key] = { env: SECRET_IN_DASHBOARD, secretPath: "" };
|
||||
});
|
||||
|
||||
return importedSec.secrets.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
overriden: overridenSec?.[key]
|
||||
}));
|
||||
};
|
||||
|
||||
type Props = {
|
||||
secrets?: DecryptedSecret[];
|
||||
importedSecrets?: TImportedSecrets;
|
||||
onSecretImportDelete: (env: string, secPath: string) => void;
|
||||
items: { id: string; environment: string; secretPath: string }[];
|
||||
};
|
||||
|
||||
export const SecretImportSection = ({
|
||||
secrets = [],
|
||||
importedSecrets = [],
|
||||
onSecretImportDelete,
|
||||
items = []
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
|
||||
return (
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
|
||||
<SecretImportItem
|
||||
key={id}
|
||||
importedEnv={importSecEnv}
|
||||
importedSecrets={computeImportedSecretRows(
|
||||
importSecEnv,
|
||||
impSecPath,
|
||||
importedSecrets,
|
||||
secrets,
|
||||
environments
|
||||
)}
|
||||
onDelete={onSecretImportDelete}
|
||||
importedSecPath={impSecPath}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretImportSection } from "./SecretImportSection";
|
||||
@@ -407,7 +407,7 @@ export const SecretInputRow = memo(
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
size="md"
|
||||
size="lg"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
|
||||
|
||||
export const RiskStatusSelection = ({riskId, currentSelection}: {riskId: any, currentSelection: any }) => {
|
||||
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
|
||||
useEffect(()=>{
|
||||
if (currentSelection !== selectedRiskStatus){
|
||||
const updateSelection = async () =>{
|
||||
await updateRiskStatus(String(localStorage.getItem("orgData.id")), riskId, selectedRiskStatus)
|
||||
}
|
||||
updateSelection()
|
||||
}
|
||||
},[selectedRiskStatus])
|
||||
|
||||
return (
|
||||
<select
|
||||
value={selectedRiskStatus}
|
||||
onChange={(e) => setSelectedRiskStatus(e.target.value)}
|
||||
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option>Unresolved</option>
|
||||
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive, resolved</option>
|
||||
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
|
||||
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr} from "@app/components/v2";
|
||||
import timeSince from "@app/ee/utilities/timeSince";
|
||||
import getRisksByOrganization, { GitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization";
|
||||
|
||||
import { RiskStatusSelection } from "./RiskStatusSelection";
|
||||
|
||||
export const SecretScanningLogsTable = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRisks = async () => {
|
||||
setIsLoading(true);
|
||||
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
|
||||
setGitRisks(risks);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
fetchRisks();
|
||||
},[])
|
||||
|
||||
|
||||
return (
|
||||
<TableContainer className="mt-8">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Date</Th>
|
||||
<Th className="flex-1">Secret Type</Th>
|
||||
<Th className="flex-1">View Risk</Th>
|
||||
<Th className="flex-1">Info</Th>
|
||||
<Th className="flex-1">Status</Th>
|
||||
<Th className="flex-1">Action</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading && gitRisks && gitRisks?.map((risk) => {
|
||||
return (
|
||||
<Tr key={risk.ruleID} className="h-10">
|
||||
<Td>{timeSince(new Date(risk.createdAt))}</Td>
|
||||
<Td>{risk.ruleID}</Td>
|
||||
<Td>
|
||||
<a
|
||||
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
||||
target="_blank"
|
||||
className="text-red-500" rel="noreferrer"
|
||||
>
|
||||
View Exposed Secret
|
||||
</a>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="font-bold">
|
||||
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
||||
{risk.repositoryFullName}
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span>{risk.file}</span><br/>
|
||||
<br/>
|
||||
<span className="font-bold">{risk.author}</span><br/>
|
||||
<span>{risk.email}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
|
||||
<Td>
|
||||
<RiskStatusSelection riskId={risk._id} currentSelection={risk.status}/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && <TableSkeleton columns={7} key="gitRisks" />}
|
||||
{!isLoading && gitRisks && gitRisks?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={7}>
|
||||
<EmptyState
|
||||
title="No risks detected."
|
||||
icon={faCheck}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretScanningLogsTable } from "./SecretScanningLogsTable";
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
export const BillingSettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full px-6">
|
||||
<div className="max-w-7xl w-full">
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
|
||||
<div />
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export const ChangeLanguageSection = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
|
||||
{t("settings.personal.change-language")}
|
||||
</p>
|
||||
|
||||
+2
@@ -3,10 +3,12 @@ import { ChangePasswordSection } from "../ChangePasswordSection";
|
||||
import { EmergencyKitSection } from "../EmergencyKitSection";
|
||||
import { SecuritySection } from "../SecuritySection";
|
||||
import { SessionsSection } from "../SessionsSection";
|
||||
import { UserNameSection } from "../UserNameSection";
|
||||
|
||||
export const PersonalSecurityTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<UserNameSection />
|
||||
<ChangeLanguageSection />
|
||||
<SecuritySection />
|
||||
<SessionsSection />
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const tabs = [
|
||||
export const PersonalTabGroup = () => {
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
|
||||
<Tab.List className="mb-4 border-b-2 border-mineshaft-800 w-full">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { useRenameUser } from "@app/hooks/api/users/queries";
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("User Name"),
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const UserNameSection = (): JSX.Element => {
|
||||
const { user } = useUser();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { mutateAsync, isLoading } = useRenameUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset({ name: `${user?.firstName}${user?.lastName && " "}${user?.lastName}` });
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
if (!user?._id) return;
|
||||
if (name === "") return;
|
||||
|
||||
await mutateAsync({ newName: name});
|
||||
createNotification({
|
||||
text: "Successfully renamed user",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to rename user",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
|
||||
User name
|
||||
</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder={`${user?.firstName} ${user?.lastName}`} {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { UserNameSection } from "./UserNameSection";
|
||||
@@ -26,15 +26,13 @@ export const ProjectSettingsPage = () => {
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
|
||||
</div>
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-6 w-full border-b-2 border-mineshaft-800">
|
||||
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 p-4 font-semibold outline-none ${
|
||||
selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"
|
||||
}`}
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
|
||||
+1
-3
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
@@ -27,7 +26,6 @@ export const ProjectNameChangeSection = () => {
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
@@ -67,7 +65,7 @@ export const ProjectNameChangeSection = () => {
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||
{t("common.display-name")}
|
||||
Project Name
|
||||
</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
|
||||
@@ -7,7 +7,7 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.2.0
|
||||
version: 0.2.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
|
||||
@@ -51,16 +51,6 @@ component: {{ .Values.frontend.name | quote }}
|
||||
{{ include "infisical.common.matchLabels" . }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.secretScanningGitApp.labels" -}}
|
||||
{{ include "infisical.secretScanningGitApp.matchLabels" . }}
|
||||
{{ include "infisical.common.metaLabels" . }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.secretScanningGitApp.matchLabels" -}}
|
||||
component: {{ .Values.secretScanningGitApp.name | quote }}
|
||||
{{ include "infisical.common.matchLabels" . }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "infisical.mongodb.labels" -}}
|
||||
{{ include "infisical.mongodb.matchLabels" . }}
|
||||
{{ include "infisical.common.metaLabels" . }}
|
||||
@@ -122,24 +112,6 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{/*
|
||||
Create a fully qualified secretScanningGitApp name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "infisical.secretScanningGitApp.fullname" -}}
|
||||
{{- if .Values.secretScanningGitApp.fullnameOverride -}}
|
||||
{{- .Values.secretScanningGitApp.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- printf "%s-%s" .Release.Name .Values.secretScanningGitApp.name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s-%s" .Release.Name $name .Values.secretScanningGitApp.name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the mongodb connection string.
|
||||
*/}}
|
||||
|
||||
@@ -44,13 +44,13 @@ spec:
|
||||
name: {{ include "infisical.backend.fullname" . }}
|
||||
port:
|
||||
number: 4000
|
||||
- path: {{ $ingress.secretScanningGitApp.path }}
|
||||
pathType: {{ $ingress.secretScanningGitApp.pathType }}
|
||||
- path: /ss-webhook
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "infisical.secretScanningGitApp.fullname" . }}
|
||||
name: {{ include "infisical.backend.fullname" . }}
|
||||
port:
|
||||
number: 3001
|
||||
number: 4000
|
||||
{{- if $ingress.hostName }}
|
||||
host: {{ $ingress.hostName }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
{{- $secretScanningGitApp := .Values.secretScanningGitApp }}
|
||||
{{- $backend := .Values.backend }}
|
||||
{{- if .Values.secretScanningGitApp.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "infisical.secretScanningGitApp.fullname" . }}
|
||||
annotations:
|
||||
updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }}
|
||||
{{- with $secretScanningGitApp.deploymentAnnotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "infisical.secretScanningGitApp.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ $secretScanningGitApp.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "infisical.secretScanningGitApp.matchLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "infisical.secretScanningGitApp.matchLabels" . | nindent 8 }}
|
||||
annotations:
|
||||
updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }}
|
||||
{{- with $secretScanningGitApp.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ template "infisical.name" . }}-{{ $secretScanningGitApp.name }}
|
||||
image: "{{ $secretScanningGitApp.image.repository }}:{{ $secretScanningGitApp.image.tag | default "latest" }}"
|
||||
imagePullPolicy: {{ $secretScanningGitApp.image.pullPolicy }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ $backend.kubeSecretRef | default (include "infisical.backend.fullname" .) }}
|
||||
{{- end }}
|
||||
---
|
||||
{{- if .Values.secretScanningGitApp.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "infisical.secretScanningGitApp.fullname" . }}
|
||||
labels:
|
||||
{{- include "infisical.secretScanningGitApp.labels" . | nindent 4 }}
|
||||
{{- with $secretScanningGitApp.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ $secretScanningGitApp.service.type }}
|
||||
selector:
|
||||
{{- include "infisical.secretScanningGitApp.matchLabels" . | nindent 8 }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 3001
|
||||
targetPort: 3000 # container port
|
||||
{{- if eq $secretScanningGitApp.service.type "NodePort" }}
|
||||
nodePort: {{ $secretScanningGitApp.service.nodePort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -169,53 +169,6 @@ backendEnvironmentVariables:
|
||||
##
|
||||
MONGO_URL: ""
|
||||
|
||||
secretScanningGitApp:
|
||||
## @param backend.enabled Enable git scanning app
|
||||
##
|
||||
enabled: false
|
||||
## @param backend.name Backend name
|
||||
##
|
||||
name: secret-scanning-git-app
|
||||
## @param backend.fullnameOverride Backend fullnameOverride
|
||||
##
|
||||
fullnameOverride: ""
|
||||
## @param backend.podAnnotations Backend pod annotations
|
||||
##
|
||||
podAnnotations: {}
|
||||
## @param backend.deploymentAnnotations Backend deployment annotations
|
||||
##
|
||||
deploymentAnnotations: {}
|
||||
## @param backend.replicaCount Backend replica count
|
||||
##
|
||||
replicaCount: 2
|
||||
## Backend image parameters
|
||||
##
|
||||
image:
|
||||
## @param backend.image.repository Backend image repository
|
||||
##
|
||||
repository: infisical/staging_deployment_secret-scanning-git-app
|
||||
## @param backend.image.tag Backend image tag
|
||||
##
|
||||
tag: "latest"
|
||||
## @param backend.image.pullPolicy Backend image pullPolicy
|
||||
##
|
||||
pullPolicy: IfNotPresent
|
||||
## @param backend.kubeSecretRef Backend secret resource reference name (containing required [backend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars))
|
||||
##
|
||||
kubeSecretRef: ""
|
||||
## Backend service
|
||||
##
|
||||
service:
|
||||
## @param backend.service.annotations Backend service annotations
|
||||
##
|
||||
annotations: {}
|
||||
## @param backend.service.type Backend service type
|
||||
##
|
||||
type: ClusterIP
|
||||
## @param backend.service.nodePort Backend service nodePort (used if above type is `NodePort`)
|
||||
##
|
||||
nodePort: ""
|
||||
|
||||
## @section MongoDB(®) parameters
|
||||
## Documentation : https://github.com/bitnami/charts/blob/main/bitnami/mongodb/values.yaml
|
||||
##
|
||||
@@ -374,11 +327,6 @@ ingress:
|
||||
backend:
|
||||
path: /api
|
||||
pathType: Prefix
|
||||
## @skip ingress.backend
|
||||
##
|
||||
secretScanningGitApp:
|
||||
path: /git-app-api
|
||||
pathType: Prefix
|
||||
## @param ingress.tls Ingress TLS hosts (matching above hostName)
|
||||
## Replace with your own domain
|
||||
##
|
||||
|
||||
@@ -18,4 +18,4 @@ version: 0.2.0
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "0.2.0"
|
||||
appVersion: "0.3.0"
|
||||
|
||||
@@ -56,6 +56,7 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
SetHeader("If-None-Match", request.ETag).
|
||||
SetQueryParam("environment", request.Environment).
|
||||
SetQueryParam("include_imports", "true"). // TODO needs to be set as a option
|
||||
SetQueryParam("workspaceId", request.WorkspaceId)
|
||||
|
||||
if request.SecretPath != "" {
|
||||
|
||||
@@ -29,43 +29,54 @@ type GetEncryptedWorkspaceKeyResponse struct {
|
||||
}
|
||||
|
||||
type GetEncryptedSecretsV3Request struct {
|
||||
Environment string `json:"environment"`
|
||||
WorkspaceId string `json:"workspaceId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
Environment string `json:"environment"`
|
||||
WorkspaceId string `json:"workspaceId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
IncludeImport bool `json:"include_imports"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
}
|
||||
|
||||
type EncryptedSecretV3 struct {
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
} `json:"tags"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
SecretKeyTag string `json:"secretKeyTag"`
|
||||
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||
SecretValueIV string `json:"secretValueIV"`
|
||||
SecretValueTag string `json:"secretValueTag"`
|
||||
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
|
||||
SecretCommentIV string `json:"secretCommentIV"`
|
||||
SecretCommentTag string `json:"secretCommentTag"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
KeyEncoding string `json:"keyEncoding"`
|
||||
Folder string `json:"folder"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ImportedSecretV3 struct {
|
||||
Environment string `json:"environment"`
|
||||
FolderId string `json:"folderId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
Secrets []EncryptedSecretV3 `json:"secrets"`
|
||||
}
|
||||
|
||||
type GetEncryptedSecretsV3Response struct {
|
||||
Secrets []struct {
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
} `json:"tags"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
SecretKeyTag string `json:"secretKeyTag"`
|
||||
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||
SecretValueIV string `json:"secretValueIV"`
|
||||
SecretValueTag string `json:"secretValueTag"`
|
||||
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
|
||||
SecretCommentIV string `json:"secretCommentIV"`
|
||||
SecretCommentTag string `json:"secretCommentTag"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
KeyEncoding string `json:"keyEncoding"`
|
||||
Folder string `json:"folder"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
} `json:"secrets"`
|
||||
Modified bool `json:"modified,omitempty"`
|
||||
ETag string `json:"ETag,omitempty"`
|
||||
Secrets []EncryptedSecretV3 `json:"secrets"`
|
||||
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
|
||||
Modified bool `json:"modified,omitempty"`
|
||||
ETag string `json:"ETag,omitempty"`
|
||||
}
|
||||
|
||||
type GetServiceTokenDetailsResponse struct {
|
||||
|
||||
@@ -89,13 +89,18 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
|
||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt the required workspace key")
|
||||
}
|
||||
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
|
||||
if err != nil {
|
||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
|
||||
}
|
||||
|
||||
plainTextSecretsMergedWithImports, err := InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecretsResponse.ImportedSecrets)
|
||||
if err != nil {
|
||||
return nil, api.GetEncryptedSecretsV3Response{}, err
|
||||
}
|
||||
|
||||
// expand secrets that are referenced
|
||||
expandedSecrets := ExpandSecrets(plainTextSecrets, fullServiceToken)
|
||||
expandedSecrets := ExpandSecrets(plainTextSecretsMergedWithImports, fullServiceToken)
|
||||
|
||||
return expandedSecrets, encryptedSecretsResponse, nil
|
||||
}
|
||||
@@ -163,7 +168,7 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
|
||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
|
||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
|
||||
if err != nil {
|
||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
|
||||
}
|
||||
@@ -200,9 +205,9 @@ func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV st
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) ([]model.SingleEnvironmentVariable, error) {
|
||||
func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) ([]model.SingleEnvironmentVariable, error) {
|
||||
plainTextSecrets := []model.SingleEnvironmentVariable{}
|
||||
for _, secret := range encryptedSecretsResponse.Secrets {
|
||||
for _, secret := range encryptedSecrets {
|
||||
// Decrypt key
|
||||
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
|
||||
if err != nil {
|
||||
@@ -332,7 +337,7 @@ func ExpandSecrets(secrets []model.SingleEnvironmentVariable, infisicalToken str
|
||||
// if not in cross reference cache, fetch it from server
|
||||
refSecs, _, err := GetPlainTextSecretsViaServiceToken(infisicalToken, "", env, secPath)
|
||||
if err != nil {
|
||||
fmt.Println("HELLO===>", "MOO", err)
|
||||
fmt.Printf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath)
|
||||
// HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
|
||||
}
|
||||
refSecsByKey := getSecretsByKeys(refSecs)
|
||||
@@ -358,3 +363,32 @@ func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]mode
|
||||
|
||||
return secretMapByName
|
||||
}
|
||||
|
||||
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []model.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]model.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
|
||||
}
|
||||
|
||||
@@ -13,20 +13,6 @@ server {
|
||||
|
||||
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
||||
}
|
||||
|
||||
# location /git-app-api {
|
||||
# proxy_set_header X-Real-RIP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# proxy_set_header Host $http_host;
|
||||
# proxy_set_header X-NginX-Proxy true;
|
||||
|
||||
# proxy_pass http://git-app:3000/;
|
||||
# proxy_redirect off;
|
||||
# # proxy_redirect http://localhost:8080/ http://frontend.example.com/;
|
||||
|
||||
# proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
||||
# }
|
||||
|
||||
location / {
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
**/node_modules/
|
||||
**/.git
|
||||
**/README.md
|
||||
**/LICENSE
|
||||
**/.vscode
|
||||
**/npm-debug.log
|
||||
**/coverage
|
||||
**/.env
|
||||
**/.editorconfig
|
||||
**/dist
|
||||
**/lib
|
||||
**/*.pem
|
||||
Dockerfile
|
||||
@@ -1,9 +0,0 @@
|
||||
# The ID of your GitHub App
|
||||
APP_ID=
|
||||
WEBHOOK_SECRET=development
|
||||
|
||||
# Use `trace` to get verbose logging or `info` to show less
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Go to https://smee.io/new set this to the URL that you are redirected to.
|
||||
WEBHOOK_PROXY_URL=
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
// "extends": [
|
||||
// "eslint:recommended",
|
||||
// "plugin:@typescript-eslint/recommended"
|
||||
// ]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
*.pem
|
||||
!mock-cert.pem
|
||||
.env
|
||||
coverage
|
||||
lib
|
||||
@@ -1,73 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance, race,
|
||||
religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at . All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
@@ -1,39 +0,0 @@
|
||||
## Contributing
|
||||
|
||||
[fork]: /fork
|
||||
[pr]: /compare
|
||||
[code-of-conduct]: CODE_OF_CONDUCT.md
|
||||
|
||||
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
|
||||
|
||||
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
|
||||
|
||||
## Issues and PRs
|
||||
|
||||
If you have suggestions for how this project could be improved, or want to report a bug, open an issue! We'd love all and any contributions. If you have questions, too, we'd love to hear them.
|
||||
|
||||
We'd also love PRs. If you're thinking of a large PR, we advise opening up an issue first to talk about it, though! Look at the links below if you're not sure how to open a PR.
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
1. [Fork][fork] and clone the repository.
|
||||
1. Configure and install the dependencies: `npm install`.
|
||||
1. Make sure the tests pass on your machine: `npm test`, note: these tests also apply the linter, so there's no need to lint separately.
|
||||
1. Create a new branch: `git checkout -b my-branch-name`.
|
||||
1. Make your change, add tests, and make sure the tests still pass.
|
||||
1. Push to your fork and [submit a pull request][pr].
|
||||
1. Pat your self on the back and wait for your pull request to be reviewed and merged.
|
||||
|
||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||
|
||||
- Write and update tests.
|
||||
- Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
|
||||
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
|
||||
Work in Progress pull requests are also welcome to get feedback early on, or if there is something blocked you.
|
||||
|
||||
## Resources
|
||||
|
||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||
- [GitHub Help](https://help.github.com)
|
||||
@@ -1,15 +0,0 @@
|
||||
FROM node:18-slim
|
||||
WORKDIR /usr/src/app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --production
|
||||
RUN npm cache clean --force
|
||||
ENV NODE_ENV="production"
|
||||
COPY . .
|
||||
|
||||
RUN apt-get update && apt-get install -y bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.8.1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
CMD [ "npm", "run", "start:prod" ]
|
||||
@@ -1,13 +0,0 @@
|
||||
FROM node:18-slim
|
||||
WORKDIR app/
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
RUN npm cache clean --force
|
||||
COPY . .
|
||||
|
||||
RUN apt-get update && apt-get install -y bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.8.1
|
||||
|
||||
|
||||
CMD [ "npm", "run", "start:dev" ]
|
||||
@@ -1,15 +0,0 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2023, Maidul Islam
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -1,33 +0,0 @@
|
||||
# secret-engine
|
||||
|
||||
> A GitHub App built with [Probot](https://github.com/probot/probot) that Infisical secret engine
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the bot
|
||||
npm start
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```sh
|
||||
# 1. Build container
|
||||
docker build -t secret-engine .
|
||||
|
||||
# 2. Start container
|
||||
docker run -e APP_ID=<app-id> -e PRIVATE_KEY=<pem-value> secret-engine
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
If you have suggestions for how secret-engine could be improved, or want to report a bug, open an issue! We'd love all and any contributions.
|
||||
|
||||
For more, check out the [Contributing Guide](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
[ISC](LICENSE) © 2023 Maidul Islam
|
||||
@@ -1,134 +0,0 @@
|
||||
# This is a GitHub App Manifest. These settings will be used by default when
|
||||
# initially configuring your GitHub App.
|
||||
#
|
||||
# NOTE: changing this file will not update your GitHub App settings.
|
||||
# You must visit github.com/settings/apps/your-app-name to edit them.
|
||||
#
|
||||
# Read more about configuring your GitHub App:
|
||||
# https://probot.github.io/docs/development/#configuring-a-github-app
|
||||
#
|
||||
# Read more about GitHub App Manifests:
|
||||
# https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/
|
||||
|
||||
# The list of events the GitHub App subscribes to.
|
||||
# Uncomment the event names below to enable them.
|
||||
default_events:
|
||||
- check_run
|
||||
# - check_suite
|
||||
# - commit_comment
|
||||
# - create
|
||||
# - delete
|
||||
# - deployment
|
||||
# - deployment_status
|
||||
# - fork
|
||||
# - gollum
|
||||
# - issue_comment
|
||||
# - issues
|
||||
- push
|
||||
# - label
|
||||
# - milestone
|
||||
# - member
|
||||
# - membership
|
||||
# - org_block
|
||||
# - organization
|
||||
# - page_build
|
||||
# - project
|
||||
# - project_card
|
||||
# - project_column
|
||||
# - public
|
||||
- pull_request
|
||||
# - pull_request_review
|
||||
# - pull_request_review_comment
|
||||
# - release
|
||||
# - repository
|
||||
# - repository_import
|
||||
# - status
|
||||
# - team
|
||||
# - team_add
|
||||
# - watch
|
||||
|
||||
# The set of permissions needed by the GitHub App. The format of the object uses
|
||||
# the permission name for the key (for example, issues) and the access type for
|
||||
# the value (for example, write).
|
||||
# Valid values are `read`, `write`, and `none`
|
||||
default_permissions:
|
||||
# Repository creation, deletion, settings, teams, and collaborators.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-administration
|
||||
# administration: read
|
||||
|
||||
# Checks on code.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-checks
|
||||
checks: write
|
||||
|
||||
# Repository contents, commits, branches, downloads, releases, and merges.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-contents
|
||||
contents: read
|
||||
|
||||
# Deployments and deployment statuses.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-deployments
|
||||
# deployments: read
|
||||
|
||||
# Issues and related comments, assignees, labels, and milestones.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-issues
|
||||
issues: write
|
||||
|
||||
# Search repositories, list collaborators, and access repository metadata.
|
||||
# https://developer.github.com/v3/apps/permissions/#metadata-permissions
|
||||
metadata: read
|
||||
|
||||
# Retrieve Pages statuses, configuration, and builds, as well as create new builds.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-pages
|
||||
# pages: read
|
||||
|
||||
# Pull requests and related comments, assignees, labels, milestones, and merges.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests
|
||||
pull_requests: write
|
||||
|
||||
# Manage the post-receive hooks for a repository.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks
|
||||
# repository_hooks: read
|
||||
|
||||
# Manage repository projects, columns, and cards.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-repository-projects
|
||||
# repository_projects: read
|
||||
|
||||
# Retrieve security vulnerability alerts.
|
||||
# https://developer.github.com/v4/object/repositoryvulnerabilityalert/
|
||||
# vulnerability_alerts: read
|
||||
|
||||
# Commit statuses.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-statuses
|
||||
# statuses: read
|
||||
|
||||
# Organization members and teams.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-members
|
||||
# members: read
|
||||
|
||||
# View and manage users blocked by the organization.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking
|
||||
# organization_user_blocking: read
|
||||
|
||||
# Manage organization projects, columns, and cards.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects
|
||||
# organization_projects: read
|
||||
|
||||
# Manage team discussions and related comments.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions
|
||||
# team_discussions: read
|
||||
|
||||
# Manage the post-receive hooks for an organization.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks
|
||||
# organization_hooks: read
|
||||
|
||||
# Get notified of, and update, content references.
|
||||
# https://developer.github.com/v3/apps/permissions/
|
||||
# organization_administration: read
|
||||
# The name of the GitHub App. Defaults to the name specified in package.json
|
||||
name: Infisical Radar
|
||||
# The homepage of your GitHub App.
|
||||
url: https://infisical.com/radar
|
||||
# A description of the GitHub App.
|
||||
description: Scan your commits for leaked secrets
|
||||
# Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app.
|
||||
# Default: true
|
||||
# public: false
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user