mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 09:26:47 +00:00
improvements: address feedback and setup queue worker profiles
This commit is contained in:
@@ -189,7 +189,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/configs/:projectId",
|
url: "/configs",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
@@ -197,7 +197,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.SecretScanning],
|
tags: [ApiDocsTags.SecretScanning],
|
||||||
description: "Get the Secret Scanning Config for the specified project.",
|
description: "Get the Secret Scanning Config for the specified project.",
|
||||||
params: z.object({
|
querystring: z.object({
|
||||||
projectId: z
|
projectId: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -213,7 +213,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const {
|
const {
|
||||||
params: { projectId },
|
query: { projectId },
|
||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/configs/:projectId",
|
url: "/configs",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
@@ -241,7 +241,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.SecretScanning],
|
tags: [ApiDocsTags.SecretScanning],
|
||||||
description: "Update the specified Secret Scanning Configuration.",
|
description: "Update the specified Secret Scanning Configuration.",
|
||||||
params: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningConfigs.UPDATE.projectId)
|
projectId: z.string().trim().min(1, "Finding ID required").describe(SecretScanningConfigs.UPDATE.projectId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
@@ -254,7 +254,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const {
|
const {
|
||||||
params: { projectId },
|
query: { projectId },
|
||||||
body,
|
body,
|
||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { ProbotOctokit } from "probot";
|
import { ProbotOctokit } from "probot";
|
||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
|
import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
|
||||||
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||||
@@ -28,6 +29,8 @@ import { listGitHubRadarRepositories, TGitHubRadarConnection } from "@app/servic
|
|||||||
|
|
||||||
import { TGitHubDataSourceWithConnection, TQueueGitHubResourceDiffScan } from "./github-secret-scanning-types";
|
import { TGitHubDataSourceWithConnection, TQueueGitHubResourceDiffScan } from "./github-secret-scanning-types";
|
||||||
|
|
||||||
|
const GitHubRepositoryRegex = new RE2(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/);
|
||||||
|
|
||||||
export const GitHubSecretScanningFactory = () => {
|
export const GitHubSecretScanningFactory = () => {
|
||||||
const initialize: TSecretScanningFactoryInitialize<TGitHubRadarConnection> = async (
|
const initialize: TSecretScanningFactoryInitialize<TGitHubRadarConnection> = async (
|
||||||
{ connection, secretScanningV2DAL },
|
{ connection, secretScanningV2DAL },
|
||||||
@@ -104,6 +107,10 @@ export const GitHubSecretScanningFactory = () => {
|
|||||||
|
|
||||||
const repoPath = join(tempFolder, "repo.git");
|
const repoPath = join(tempFolder, "repo.git");
|
||||||
|
|
||||||
|
if (!GitHubRepositoryRegex.test(resourceName)) {
|
||||||
|
throw new Error("Invalid GitHub repository name");
|
||||||
|
}
|
||||||
|
|
||||||
await cloneRepository({
|
await cloneRepository({
|
||||||
cloneUrl: `https://x-access-token:${token}@github.com/${resourceName}.git`,
|
cloneUrl: `https://x-access-token:${token}@github.com/${resourceName}.git`,
|
||||||
repoPath
|
repoPath
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export const githubSecretScanningService = (
|
|||||||
) => {
|
) => {
|
||||||
const handleInstallationDeletedEvent = async (installationId: number) => {
|
const handleInstallationDeletedEvent = async (installationId: number) => {
|
||||||
const dataSource = await secretScanningV2DAL.dataSources.findOne({
|
const dataSource = await secretScanningV2DAL.dataSources.findOne({
|
||||||
externalId: String(installationId)
|
externalId: String(installationId),
|
||||||
|
type: SecretScanningDataSource.GitHub
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { readFindingsFile } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
|
import { readFindingsFile } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
|
||||||
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
|
||||||
@@ -83,6 +84,8 @@ export const replaceNonChangesWithNewlines = (patch: string) => {
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const HunkHeaderRegex = new RE2(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
||||||
|
|
||||||
export const convertPatchLineToFileLineNumber = (patch: string, patchLineNumber: number) => {
|
export const convertPatchLineToFileLineNumber = (patch: string, patchLineNumber: number) => {
|
||||||
const lines = patch.split("\n");
|
const lines = patch.split("\n");
|
||||||
let currentPatchLine = 0;
|
let currentPatchLine = 0;
|
||||||
@@ -92,7 +95,7 @@ export const convertPatchLineToFileLineNumber = (patch: string, patchLineNumber:
|
|||||||
currentPatchLine += 1;
|
currentPatchLine += 1;
|
||||||
|
|
||||||
// Hunk header: @@ -a,b +c,d @@
|
// Hunk header: @@ -a,b +c,d @@
|
||||||
const hunkHeaderMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
const hunkHeaderMatch = line.match(HunkHeaderRegex);
|
||||||
if (hunkHeaderMatch) {
|
if (hunkHeaderMatch) {
|
||||||
const startLine = parseInt(hunkHeaderMatch[1], 10);
|
const startLine = parseInt(hunkHeaderMatch[1], 10);
|
||||||
currentNewLine = startLine;
|
currentNewLine = startLine;
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
|||||||
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
|
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
|
||||||
import {
|
import {
|
||||||
SecretScanningDataSource,
|
SecretScanningDataSource,
|
||||||
SecretScanningFindingStatus,
|
|
||||||
SecretScanningResource,
|
SecretScanningResource,
|
||||||
SecretScanningScanStatus,
|
SecretScanningScanStatus,
|
||||||
SecretScanningScanType
|
SecretScanningScanType
|
||||||
@@ -217,12 +216,11 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
dataSourceType: dataSource.type,
|
dataSourceType: dataSource.type,
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
resourceType: resource.type,
|
resourceType: resource.type,
|
||||||
scanId,
|
scanId
|
||||||
status: SecretScanningFindingStatus.Unresolved
|
|
||||||
})),
|
})),
|
||||||
["projectId", "fingerprint"],
|
["projectId", "fingerprint"],
|
||||||
tx,
|
tx,
|
||||||
["resourceName", "dataSourceName", "status"]
|
["resourceName", "dataSourceName"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,12 +439,11 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
dataSourceType: dataSource.type,
|
dataSourceType: dataSource.type,
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
resourceType: resource.type,
|
resourceType: resource.type,
|
||||||
scanId,
|
scanId
|
||||||
status: SecretScanningFindingStatus.Unresolved
|
|
||||||
})),
|
})),
|
||||||
["projectId", "fingerprint"],
|
["projectId", "fingerprint"],
|
||||||
tx,
|
tx,
|
||||||
["resourceName", "dataSourceName", "status"]
|
["resourceName", "dataSourceName"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -9,6 +10,12 @@ import {
|
|||||||
ProjectPermissionSecretScanningFindingActions,
|
ProjectPermissionSecretScanningFindingActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import {
|
||||||
|
createTempFolder,
|
||||||
|
deleteTempFolder,
|
||||||
|
scanContentAndGetFindings,
|
||||||
|
writeTextToFile
|
||||||
|
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
|
||||||
import { githubSecretScanningService } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-service";
|
import { githubSecretScanningService } from "@app/ee/services/secret-scanning-v2/github/github-secret-scanning-service";
|
||||||
import { SecretScanningFindingStatus } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
import { SecretScanningFindingStatus } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-enums";
|
||||||
import { SECRET_SCANNING_FACTORY_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-factory";
|
import { SECRET_SCANNING_FACTORY_MAP } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-factory";
|
||||||
@@ -810,10 +817,26 @@ export const secretScanningV2ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionSecretScanningConfigActions.Read,
|
ProjectPermissionSecretScanningConfigActions.Update,
|
||||||
ProjectPermissionSub.SecretScanningConfigs
|
ProjectPermissionSub.SecretScanningConfigs
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
const tempFolder = await createTempFolder();
|
||||||
|
try {
|
||||||
|
const configPath = join(tempFolder, "infisical-scan.toml");
|
||||||
|
await writeTextToFile(configPath, content);
|
||||||
|
|
||||||
|
await scanContentAndGetFindings("", configPath);
|
||||||
|
} catch (e) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to parse configuration: Check syntax and formatting."
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await deleteTempFolder(tempFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [config] = await secretScanningV2DAL.configs.upsert(
|
const [config] = await secretScanningV2DAL.configs.upsert(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { QueueWorkerProfile } from "@app/lib/types";
|
||||||
|
|
||||||
import { removeTrailingSlash } from "../fn";
|
import { removeTrailingSlash } from "../fn";
|
||||||
import { CustomLogger } from "../logger/logger";
|
import { CustomLogger } from "../logger/logger";
|
||||||
import { zpStr } from "../zod";
|
import { zpStr } from "../zod";
|
||||||
@@ -57,6 +59,7 @@ const envSchema = z
|
|||||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||||
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
|
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
|
||||||
|
QUEUE_WORKER_PROFILE: z.nativeEnum(QueueWorkerProfile).default(QueueWorkerProfile.All),
|
||||||
HTTPS_ENABLED: zodStrBool,
|
HTTPS_ENABLED: zodStrBool,
|
||||||
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
|
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
|
||||||
// smtp options
|
// smtp options
|
||||||
|
|||||||
@@ -78,3 +78,9 @@ export type OrgServiceActor = {
|
|||||||
authMethod: ActorAuthMethod;
|
authMethod: ActorAuthMethod;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum QueueWorkerProfile {
|
||||||
|
All = "all",
|
||||||
|
Standard = "standard",
|
||||||
|
SecretScanning = "secret-scanning"
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
} from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-types";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueWorkerProfile } from "@app/lib/types";
|
||||||
import {
|
import {
|
||||||
TFailedIntegrationSyncEmailsPayload,
|
TFailedIntegrationSyncEmailsPayload,
|
||||||
TIntegrationSyncPayload,
|
TIntegrationSyncPayload,
|
||||||
@@ -269,6 +270,37 @@ export type TQueueJobTypes = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SECRET_SCANNING_JOBS = [
|
||||||
|
QueueJobs.SecretScanningV2FullScan,
|
||||||
|
QueueJobs.SecretScanningV2DiffScan,
|
||||||
|
QueueJobs.SecretScanningV2SendNotification,
|
||||||
|
QueueJobs.SecretScan
|
||||||
|
];
|
||||||
|
|
||||||
|
const NON_STANDARD_JOBS = [...SECRET_SCANNING_JOBS];
|
||||||
|
|
||||||
|
const SECRET_SCANNING_QUEUES = [
|
||||||
|
QueueName.SecretScanningV2,
|
||||||
|
QueueName.SecretFullRepoScan,
|
||||||
|
QueueName.SecretPushEventScan
|
||||||
|
];
|
||||||
|
|
||||||
|
const NON_STANDARD_QUEUES = [...SECRET_SCANNING_QUEUES];
|
||||||
|
|
||||||
|
const isQueueEnabled = (name: QueueName) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
switch (appCfg.QUEUE_WORKER_PROFILE) {
|
||||||
|
case QueueWorkerProfile.Standard:
|
||||||
|
return !NON_STANDARD_QUEUES.includes(name);
|
||||||
|
case QueueWorkerProfile.SecretScanning:
|
||||||
|
return SECRET_SCANNING_QUEUES.includes(name);
|
||||||
|
case QueueWorkerProfile.All:
|
||||||
|
default:
|
||||||
|
// allow all
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
export const queueServiceFactory = (
|
export const queueServiceFactory = (
|
||||||
redisUrl: string,
|
redisUrl: string,
|
||||||
@@ -325,7 +357,7 @@ export const queueServiceFactory = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (appCfg.QUEUE_WORKERS_ENABLED) {
|
if (appCfg.QUEUE_WORKERS_ENABLED && isQueueEnabled(name)) {
|
||||||
workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, {
|
workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, {
|
||||||
...queueSettings,
|
...queueSettings,
|
||||||
connection
|
connection
|
||||||
@@ -344,6 +376,30 @@ export const queueServiceFactory = (
|
|||||||
throw new Error(`${jobName} queue is already initialized`);
|
throw new Error(`${jobName} queue is already initialized`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
if (!appCfg.QUEUE_WORKERS_ENABLED) return;
|
||||||
|
|
||||||
|
switch (appCfg.QUEUE_WORKER_PROFILE) {
|
||||||
|
case QueueWorkerProfile.Standard:
|
||||||
|
if (NON_STANDARD_JOBS.includes(jobName)) {
|
||||||
|
// only process standard jobs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case QueueWorkerProfile.SecretScanning:
|
||||||
|
if (!SECRET_SCANNING_JOBS.includes(jobName)) {
|
||||||
|
// only process secret scanning jobs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case QueueWorkerProfile.All:
|
||||||
|
default:
|
||||||
|
// allow all
|
||||||
|
}
|
||||||
|
|
||||||
await pgBoss.createQueue(jobName);
|
await pgBoss.createQueue(jobName);
|
||||||
queueContainerPg[jobName] = true;
|
queueContainerPg[jobName] = true;
|
||||||
|
|
||||||
@@ -363,7 +419,7 @@ export const queueServiceFactory = (
|
|||||||
listener: WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>[U]
|
listener: WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>[U]
|
||||||
) => {
|
) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
if (!appCfg.QUEUE_WORKERS_ENABLED) {
|
if (!appCfg.QUEUE_WORKERS_ENABLED || !isQueueEnabled(name)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
title: "Get by Project ID"
|
title: "Get by Project ID"
|
||||||
openapi: "GET /api/v2/secret-scanning/configs/{projectId}"
|
openapi: "GET /api/v2/secret-scanning/configs"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Update"
|
title: "Update"
|
||||||
openapi: "PATCH /api/v2/secret-scanning/configs/{projectId}"
|
openapi: "PATCH /api/v2/secret-scanning/configs"
|
||||||
---
|
---
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ Monitor and detect exposed secrets across your data sources, including code repo
|
|||||||
|
|
||||||
For additional security, we recommend using our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to check for exposed secrets before pushing your code changes.
|
For additional security, we recommend using our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to check for exposed secrets before pushing your code changes.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Secret Scanning is a paid feature.
|
||||||
|
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||||
|
then you should contact [email protected] to purchase an enterprise license to use it.
|
||||||
|
</Note>
|
||||||
|
|
||||||
## How Secret Scanning Works
|
## How Secret Scanning Works
|
||||||
|
|
||||||
Secret Scanning consists of several components that enable you to quickly respond to secret leaks:
|
Secret Scanning consists of several components that enable you to quickly respond to secret leaks:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 519 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 500 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 520 KiB After Width: | Height: | Size: 455 KiB |
@@ -5,6 +5,12 @@ description: "Learn how to configure a GitHub Radar Connection for Infisical."
|
|||||||
|
|
||||||
Infisical supports GitHub App installation for creating a GitHub Radar Connection.
|
Infisical supports GitHub App installation for creating a GitHub Radar Connection.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
GitHub Radar Connections are specifically configured for [Secret Scanning](/documentation/platform/secret-scanning/overview) and require specific permissions and webhook configuration.
|
||||||
|
|
||||||
|
Check out our [GitHub Connection](/integrations/app-connections/github) for secret management features such as [Secret Syncs](/integrations/secret-syncs/overview).
|
||||||
|
</Note>
|
||||||
|
|
||||||
<Accordion title="Self-Hosted Instance">
|
<Accordion title="Self-Hosted Instance">
|
||||||
Using a GitHub Radar Connection with app authentication on a self-hosted instance of Infisical requires configuring an application on GitHub
|
Using a GitHub Radar Connection with app authentication on a self-hosted instance of Infisical requires configuring an application on GitHub
|
||||||
and registering your instance with it.
|
and registering your instance with it.
|
||||||
@@ -38,19 +44,14 @@ Infisical supports GitHub App installation for creating a GitHub Radar Connectio
|
|||||||

|

|
||||||
|
|
||||||
Set the following repository permissions:
|
Set the following repository permissions:
|
||||||
1. **Checks**: `Read and Write`
|
- **Contents**: `Read-only`
|
||||||
2. **Content**: `Read-only`
|
- **Metadata**: `Read-only`
|
||||||
3. **Issues**: `Read and Write`
|
|
||||||
4. **Metadata**: `Read-only`
|
|
||||||
5. **Pull Requests**: `Read and Write`
|
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
Subscribe to the following events:
|
Subscribe to the following events:
|
||||||
1. **Check run**
|
- **Push**
|
||||||
2. **Pull request**
|
|
||||||
3. **Push**
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ Infisical supports GitHub App installation for creating a GitHub Radar Connectio
|
|||||||
</Steps>
|
</Steps>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
## Setup GitHub Connection in Infisical
|
## Setup GitHub Radar Connection in Infisical
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Navigate to App Connections">
|
<Step title="Navigate to App Connections">
|
||||||
@@ -100,7 +101,7 @@ Infisical supports GitHub App installation for creating a GitHub Radar Connectio
|
|||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Add Connection">
|
<Step title="Add Connection">
|
||||||
Select the **GitHub Connection** option from the connection options modal.
|
Select the **GitHub Radar Connection** option from the connection options modal.
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Authorize Connection">
|
<Step title="Authorize Connection">
|
||||||
|
|||||||
@@ -314,3 +314,32 @@ Supports conditions and permission inversion
|
|||||||
| `create` | Create new SSH certificate templates |
|
| `create` | Create new SSH certificate templates |
|
||||||
| `edit` | Modify SSH template configurations |
|
| `edit` | Modify SSH template configurations |
|
||||||
| `delete` | Remove SSH certificate templates |
|
| `delete` | Remove SSH certificate templates |
|
||||||
|
|
||||||
|
### Secret Scanning
|
||||||
|
|
||||||
|
#### Subject: `secret-scanning-data-sources`
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
| -------- | ---------------------------------------------------- |
|
||||||
|
| `read-data-sources` | View Data Sources |
|
||||||
|
| `create-data-sources` | Create new Data Sources |
|
||||||
|
| `edit-data-sources` | Modify Data Sources |
|
||||||
|
| `delete-data-sources` | Remove Data Sources |
|
||||||
|
| `read-data-source-resources` | View Data Source Resources |
|
||||||
|
| `read-data-source-scans` | View Data Source Scans |
|
||||||
|
| `trigger-data-source-scans` | Trigger Data Source Secret Scans |
|
||||||
|
|
||||||
|
#### Subject: `secret-scanning-findings`
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
| -------- | --------------------------------- |
|
||||||
|
| `read-findings` | View Secret Scanning Findings |
|
||||||
|
| `update-findings` | Update Secret Scanning Findings |
|
||||||
|
|
||||||
|
|
||||||
|
#### Subject: `secret-scanning-configs`
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
| ---------------- | ------------------------------------------------ |
|
||||||
|
| `read-configs` | View Secret Scanning Project Configuration |
|
||||||
|
| `update-configs` | Update Secret Scanning Project Configuration |
|
||||||
|
|||||||
+1
-1
@@ -102,7 +102,7 @@ export const GitHubDataSourceConfigFields = () => {
|
|||||||
isLoading={areRepositoriesLoading && Boolean(connectionId)}
|
isLoading={areRepositoriesLoading && Boolean(connectionId)}
|
||||||
isDisabled={!connectionId}
|
isDisabled={!connectionId}
|
||||||
isMulti
|
isMulti
|
||||||
value={repositories?.find((project) => value.includes(project.name))}
|
value={repositories?.filter((repository) => value.includes(repository.name))}
|
||||||
onChange={(newValue) => {
|
onChange={(newValue) => {
|
||||||
onChange(
|
onChange(
|
||||||
newValue
|
newValue
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export const useUpdateSecretScanningConfig = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ projectId, ...params }: TSecretScanningConfig) => {
|
mutationFn: async ({ projectId, ...params }: TSecretScanningConfig) => {
|
||||||
const { data } = await apiRequest.patch<TGetSecretScanningConfigResponse>(
|
const { data } = await apiRequest.patch<TGetSecretScanningConfigResponse>(
|
||||||
`/api/v2/secret-scanning/configs/${projectId}`,
|
`/api/v2/secret-scanning/configs?projectId=${projectId}`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -237,7 +237,8 @@ export const useGetSecretScanningConfig = (
|
|||||||
queryKey: secretScanningV2Keys.configByProjectId(projectId),
|
queryKey: secretScanningV2Keys.configByProjectId(projectId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<TGetSecretScanningConfigResponse>(
|
const { data } = await apiRequest.get<TGetSecretScanningConfigResponse>(
|
||||||
`/api/v2/secret-scanning/configs/${projectId}`
|
"/api/v2/secret-scanning/configs",
|
||||||
|
{ params: { projectId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.config;
|
return data.config;
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./DataSourceConfigDisplay";
|
||||||
|
|||||||
+2
-1
@@ -9,7 +9,8 @@ import { ProjectPermissionSub } from "@app/context";
|
|||||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||||
import { DataSourceConfigDisplay } from "@app/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay";
|
|
||||||
|
import { DataSourceConfigDisplay } from "./DataSourceConfigDisplay";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dataSource: TSecretScanningDataSource;
|
dataSource: TSecretScanningDataSource;
|
||||||
|
|||||||
+77
-54
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
|
faBan,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCopy,
|
faCopy,
|
||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
@@ -50,6 +51,13 @@ export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => {
|
|||||||
const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } =
|
const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } =
|
||||||
resource;
|
resource;
|
||||||
|
|
||||||
|
const {
|
||||||
|
config: { includeRepos }
|
||||||
|
} = dataSource;
|
||||||
|
|
||||||
|
// scott: will need to be differentiated by type once other data sources are available
|
||||||
|
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
|
||||||
|
|
||||||
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
|
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -175,61 +183,76 @@ export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Tooltip className="max-w-sm text-center" content="Options">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<DropdownMenu>
|
{!isActive && (
|
||||||
<DropdownMenuTrigger asChild>
|
<Tooltip
|
||||||
<IconButton
|
className="text-xs"
|
||||||
ariaLabel="Options"
|
content={`This ${resourceDetails.singularNoun} will not be scanned due to exclusion in Data Source configuration.`}
|
||||||
colorSchema="secondary"
|
>
|
||||||
className="w-6"
|
<div className="ml-auto">
|
||||||
variant="plain"
|
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||||
>
|
<FontAwesomeIcon icon={faBan} />
|
||||||
<FontAwesomeIcon icon={faEllipsisV} />
|
<span>Inactive</span>
|
||||||
</IconButton>
|
</Badge>
|
||||||
</DropdownMenuTrigger>
|
</div>
|
||||||
<DropdownMenuContent sideOffset={2} align="end">
|
</Tooltip>
|
||||||
<DropdownMenuItem
|
)}
|
||||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
<Tooltip className="max-w-sm text-center" content="Options">
|
||||||
onClick={(e) => {
|
<DropdownMenu>
|
||||||
e.stopPropagation();
|
<DropdownMenuTrigger asChild>
|
||||||
handleCopyId(id);
|
<IconButton
|
||||||
}}
|
ariaLabel="Options"
|
||||||
>
|
colorSchema="secondary"
|
||||||
Copy Resource ID
|
className="w-6"
|
||||||
</DropdownMenuItem>
|
variant="plain"
|
||||||
<ProjectPermissionCan
|
>
|
||||||
I={ProjectPermissionSecretScanningDataSourceActions.TriggerScans}
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
</IconButton>
|
||||||
>
|
</DropdownMenuTrigger>
|
||||||
{(isAllowed) => (
|
<DropdownMenuContent sideOffset={2} align="end">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
isDisabled={!isAllowed}
|
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||||
icon={<FontAwesomeIcon icon={faExpand} />}
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
handleCopyId(id);
|
||||||
handleTriggerScan();
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Copy Resource ID
|
||||||
<Tooltip
|
</DropdownMenuItem>
|
||||||
position="left"
|
<ProjectPermissionCan
|
||||||
sideOffset={42}
|
I={ProjectPermissionSecretScanningDataSourceActions.TriggerScans}
|
||||||
content={`Manually trigger a scan for this ${resourceDetails.singularNoun}.`}
|
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
isDisabled={!isAllowed || !isActive}
|
||||||
|
icon={<FontAwesomeIcon icon={faExpand} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleTriggerScan();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full items-center justify-between gap-1">
|
<Tooltip
|
||||||
<span> Trigger Scan</span>
|
position="left"
|
||||||
<FontAwesomeIcon
|
sideOffset={42}
|
||||||
className="text-bunker-300"
|
content={`Manually trigger a scan for this ${resourceDetails.singularNoun}.`}
|
||||||
size="sm"
|
>
|
||||||
icon={faInfoCircle}
|
<div className="flex h-full w-full items-center justify-between gap-1">
|
||||||
/>
|
<span> Trigger Scan</span>
|
||||||
</div>
|
<FontAwesomeIcon
|
||||||
</Tooltip>
|
className="text-bunker-300"
|
||||||
</DropdownMenuItem>
|
size="sm"
|
||||||
)}
|
icon={faInfoCircle}
|
||||||
</ProjectPermissionCan>
|
/>
|
||||||
</DropdownMenuContent>
|
</div>
|
||||||
</DropdownMenu>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
|
|||||||
+104
-9
@@ -1,8 +1,12 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faBan,
|
faBan,
|
||||||
|
faBullseye,
|
||||||
|
faCheckCircle,
|
||||||
|
faFilter,
|
||||||
|
faInfoCircle,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faSearch
|
faSearch
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -10,6 +14,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
@@ -20,6 +29,7 @@ import {
|
|||||||
TBody,
|
TBody,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionSub, useProjectPermission } from "@app/context";
|
import { ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
@@ -45,6 +55,15 @@ type Props = {
|
|||||||
dataSource: TSecretScanningDataSource;
|
dataSource: TSecretScanningDataSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum ResourceStatus {
|
||||||
|
Active = "active",
|
||||||
|
Inactive = "inactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceFilters = {
|
||||||
|
status: ResourceStatus[];
|
||||||
|
};
|
||||||
|
|
||||||
export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
@@ -53,6 +72,10 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
|||||||
ProjectPermissionSub.SecretScanningDataSources
|
ProjectPermissionSub.SecretScanningDataSources
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState<ResourceFilters>({
|
||||||
|
status: [ResourceStatus.Active]
|
||||||
|
});
|
||||||
|
|
||||||
const { data: resources = [], isPending: isResourcesPending } = useListSecretScanningResources(
|
const { data: resources = [], isPending: isResourcesPending } = useListSecretScanningResources(
|
||||||
{ dataSourceId: dataSource.id, type: dataSource.type },
|
{ dataSourceId: dataSource.id, type: dataSource.type },
|
||||||
{
|
{
|
||||||
@@ -81,6 +104,19 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
|||||||
resources
|
resources
|
||||||
.filter((resource) => {
|
.filter((resource) => {
|
||||||
const { name } = resource;
|
const { name } = resource;
|
||||||
|
const {
|
||||||
|
config: { includeRepos }
|
||||||
|
} = dataSource;
|
||||||
|
|
||||||
|
// scott: will need to be differentiated by type once other data sources are available
|
||||||
|
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
|
||||||
|
|
||||||
|
if (filters.status.length === 1) {
|
||||||
|
if (filters.status.includes(ResourceStatus.Active)) {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
return !isActive;
|
||||||
|
}
|
||||||
|
|
||||||
const searchValue = search.trim().toLowerCase();
|
const searchValue = search.trim().toLowerCase();
|
||||||
|
|
||||||
@@ -114,7 +150,7 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
|||||||
return resourceOne.name.toLowerCase().localeCompare(resourceTwo.name.toLowerCase());
|
return resourceOne.name.toLowerCase().localeCompare(resourceTwo.name.toLowerCase());
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[resources, orderDirection, search, orderBy]
|
[resources, orderDirection, search, orderBy, filters, dataSource]
|
||||||
);
|
);
|
||||||
|
|
||||||
useResetPageHelper({
|
useResetPageHelper({
|
||||||
@@ -141,15 +177,74 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
|||||||
|
|
||||||
const resourceDetails = RESOURCE_DESCRIPTION_HELPER[dataSource.type];
|
const resourceDetails = RESOURCE_DESCRIPTION_HELPER[dataSource.type];
|
||||||
|
|
||||||
|
const isTableFiltered = Boolean(filters.status.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<div className="flex gap-2">
|
||||||
value={search}
|
<Input
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
value={search}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder={`Search ${resourceDetails.pluralNoun}...`}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
className="flex-1"
|
placeholder={`Search ${resourceDetails.pluralNoun}...`}
|
||||||
/>
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Filter data sources"
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||||
|
isTableFiltered && "border-primary/50 text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||||
|
<DropdownMenuLabel className="flex w-full items-center justify-between">
|
||||||
|
Status
|
||||||
|
<Tooltip
|
||||||
|
content={`Inactive ${resourceDetails.pluralNoun} will not be scanned due to exclusion in Data Source configuration.`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{Object.values(ResourceStatus).map((status) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
status: prev.status.includes(status)
|
||||||
|
? prev.status.filter((s) => s !== status)
|
||||||
|
: [...prev.status, status]
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
key={status}
|
||||||
|
icon={
|
||||||
|
filters.status.includes(status) && (
|
||||||
|
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={status === ResourceStatus.Active ? faBullseye : faBan}
|
||||||
|
className={
|
||||||
|
status === ResourceStatus.Active ? "text-primary" : "text-mineshaft-400"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="capitalize">{status.replace("-", " ")}</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
|
|||||||
+4
-3
@@ -24,10 +24,11 @@ export const SecretScanningDataSourcesSection = () => {
|
|||||||
|
|
||||||
const { data: dataSources = [], isPending: isDataSourcesPending } =
|
const { data: dataSources = [], isPending: isDataSourcesPending } =
|
||||||
useListSecretScanningDataSources(currentWorkspace.id, {
|
useListSecretScanningDataSources(currentWorkspace.id, {
|
||||||
refetchInterval: 30000
|
refetchInterval: 30000,
|
||||||
|
enabled: subscription.secretScanning
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDataSourcesPending)
|
if (subscription.secretScanning && isDataSourcesPending)
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[60vh] flex-col items-center justify-center gap-2">
|
<div className="flex h-[60vh] flex-col items-center justify-center gap-2">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -57,7 +58,7 @@ export const SecretScanningDataSourcesSection = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-bunker-300">
|
<p className="text-sm text-bunker-300">
|
||||||
Use App Connections to scan for secret leaks from third-party services.
|
Configure Data Sources to scan for secret leaks from third-party services.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
|
|||||||
-2
@@ -45,8 +45,6 @@ import {
|
|||||||
|
|
||||||
import { SecretScanningDataSourceRow } from "./SecretScanningDataSourceRow";
|
import { SecretScanningDataSourceRow } from "./SecretScanningDataSourceRow";
|
||||||
|
|
||||||
// import { getSecretSyncDestinationColValues } from "./helpers";
|
|
||||||
|
|
||||||
enum DataSourcesOrderBy {
|
enum DataSourcesOrderBy {
|
||||||
Findings = "findings",
|
Findings = "findings",
|
||||||
Name = "name",
|
Name = "name",
|
||||||
|
|||||||
+2
-2
@@ -64,7 +64,7 @@ export const DeleteProjectSection = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
to: `/organization/${ProjectType.SecretScanning}/overview` as const
|
||||||
});
|
});
|
||||||
handlePopUpClose("deleteWorkspace");
|
handlePopUpClose("deleteWorkspace");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -123,7 +123,7 @@ export const DeleteProjectSection = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
to: `/organization/${ProjectType.SecretScanning}/overview` as const
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
import { ProjectSshConfigCasSection } from "./components";
|
|
||||||
|
|
||||||
export const ProjectSshTab = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ProjectSshConfigCasSection />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-139
@@ -1,139 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
|
||||||
import {
|
|
||||||
useGetProjectSshConfig,
|
|
||||||
useListWorkspaceSshCas,
|
|
||||||
useUpdateProjectSshConfig
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
const schema = z
|
|
||||||
.object({
|
|
||||||
defaultUserSshCaId: z.string(),
|
|
||||||
defaultHostSshCaId: z.string()
|
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
export const ProjectSshConfigCasSection = () => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { data: sshConfig } = useGetProjectSshConfig(currentWorkspace.id);
|
|
||||||
const { data: sshCas } = useListWorkspaceSshCas(currentWorkspace.id);
|
|
||||||
const { mutate: updateProjectSshConfig } = useUpdateProjectSshConfig();
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<FormData>({
|
|
||||||
resolver: zodResolver(schema)
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sshConfig) {
|
|
||||||
reset({
|
|
||||||
defaultUserSshCaId: sshConfig.defaultUserSshCaId || "",
|
|
||||||
defaultHostSshCaId: sshConfig.defaultHostSshCaId || ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [sshConfig]);
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ defaultUserSshCaId, defaultHostSshCaId }: FormData) => {
|
|
||||||
try {
|
|
||||||
await updateProjectSshConfig({
|
|
||||||
projectId: currentWorkspace.id,
|
|
||||||
defaultUserSshCaId: defaultUserSshCaId || undefined,
|
|
||||||
defaultHostSshCaId: defaultHostSshCaId || undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated SSH project settings",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to update SSH project settings",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
|
||||||
<p className="mb-8 text-xl font-semibold">Certificate Authorities</p>
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="defaultUserSshCaId"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Default User CA"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="min-w-[20rem]"
|
|
||||||
>
|
|
||||||
{sshCas?.map(({ id, friendlyName }) => (
|
|
||||||
<SelectItem value={String(id || "")} key={friendlyName}>
|
|
||||||
{friendlyName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="defaultHostSshCaId"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Default Host CA"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="min-w-[20rem]"
|
|
||||||
>
|
|
||||||
{sshCas?.map(({ id, friendlyName }) => (
|
|
||||||
<SelectItem value={String(id || "")} key={friendlyName}>
|
|
||||||
{friendlyName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting || !isAllowed}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export { ProjectSshConfigCasSection } from "./ProjectSshConfigCasSection";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ProjectSshTab } from "./ProjectSshTab";
|
|
||||||
Reference in New Issue
Block a user