mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +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({
|
||||
method: "GET",
|
||||
url: "/configs/:projectId",
|
||||
url: "/configs",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -197,7 +197,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
description: "Get the Secret Scanning Config for the specified project.",
|
||||
params: z.object({
|
||||
querystring: z.object({
|
||||
projectId: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -213,7 +213,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { projectId },
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
@@ -233,7 +233,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/configs/:projectId",
|
||||
url: "/configs",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -241,7 +241,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SecretScanning],
|
||||
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)
|
||||
}),
|
||||
body: z.object({
|
||||
@@ -254,7 +254,7 @@ export const registerSecretScanningV2Router = async (server: FastifyZodProvider)
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { projectId },
|
||||
query: { projectId },
|
||||
body,
|
||||
permission
|
||||
} = req;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { join } from "path";
|
||||
import { ProbotOctokit } from "probot";
|
||||
import RE2 from "re2";
|
||||
|
||||
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";
|
||||
@@ -28,6 +29,8 @@ import { listGitHubRadarRepositories, TGitHubRadarConnection } from "@app/servic
|
||||
|
||||
import { TGitHubDataSourceWithConnection, TQueueGitHubResourceDiffScan } from "./github-secret-scanning-types";
|
||||
|
||||
const GitHubRepositoryRegex = new RE2(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/);
|
||||
|
||||
export const GitHubSecretScanningFactory = () => {
|
||||
const initialize: TSecretScanningFactoryInitialize<TGitHubRadarConnection> = async (
|
||||
{ connection, secretScanningV2DAL },
|
||||
@@ -104,6 +107,10 @@ export const GitHubSecretScanningFactory = () => {
|
||||
|
||||
const repoPath = join(tempFolder, "repo.git");
|
||||
|
||||
if (!GitHubRepositoryRegex.test(resourceName)) {
|
||||
throw new Error("Invalid GitHub repository name");
|
||||
}
|
||||
|
||||
await cloneRepository({
|
||||
cloneUrl: `https://x-access-token:${token}@github.com/${resourceName}.git`,
|
||||
repoPath
|
||||
|
||||
@@ -13,7 +13,8 @@ export const githubSecretScanningService = (
|
||||
) => {
|
||||
const handleInstallationDeletedEvent = async (installationId: number) => {
|
||||
const dataSource = await secretScanningV2DAL.dataSources.findOne({
|
||||
externalId: String(installationId)
|
||||
externalId: String(installationId),
|
||||
type: SecretScanningDataSource.GitHub
|
||||
});
|
||||
|
||||
if (!dataSource) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { exec } from "child_process";
|
||||
import RE2 from "re2";
|
||||
|
||||
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";
|
||||
@@ -83,6 +84,8 @@ export const replaceNonChangesWithNewlines = (patch: string) => {
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
const HunkHeaderRegex = new RE2(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
||||
|
||||
export const convertPatchLineToFileLineNumber = (patch: string, patchLineNumber: number) => {
|
||||
const lines = patch.split("\n");
|
||||
let currentPatchLine = 0;
|
||||
@@ -92,7 +95,7 @@ export const convertPatchLineToFileLineNumber = (patch: string, patchLineNumber:
|
||||
currentPatchLine += 1;
|
||||
|
||||
// Hunk header: @@ -a,b +c,d @@
|
||||
const hunkHeaderMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
||||
const hunkHeaderMatch = line.match(HunkHeaderRegex);
|
||||
if (hunkHeaderMatch) {
|
||||
const startLine = parseInt(hunkHeaderMatch[1], 10);
|
||||
currentNewLine = startLine;
|
||||
|
||||
@@ -28,7 +28,6 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TSecretScanningV2DALFactory } from "./secret-scanning-v2-dal";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningFindingStatus,
|
||||
SecretScanningResource,
|
||||
SecretScanningScanStatus,
|
||||
SecretScanningScanType
|
||||
@@ -217,12 +216,11 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
dataSourceType: dataSource.type,
|
||||
resourceName: resource.name,
|
||||
resourceType: resource.type,
|
||||
scanId,
|
||||
status: SecretScanningFindingStatus.Unresolved
|
||||
scanId
|
||||
})),
|
||||
["projectId", "fingerprint"],
|
||||
tx,
|
||||
["resourceName", "dataSourceName", "status"]
|
||||
["resourceName", "dataSourceName"]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -441,12 +439,11 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
dataSourceType: dataSource.type,
|
||||
resourceName: resource.name,
|
||||
resourceType: resource.type,
|
||||
scanId,
|
||||
status: SecretScanningFindingStatus.Unresolved
|
||||
scanId
|
||||
})),
|
||||
["projectId", "fingerprint"],
|
||||
tx,
|
||||
["resourceName", "dataSourceName", "status"]
|
||||
["resourceName", "dataSourceName"]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { join } from "path";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
@@ -9,6 +10,12 @@ import {
|
||||
ProjectPermissionSecretScanningFindingActions,
|
||||
ProjectPermissionSub
|
||||
} 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 { 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";
|
||||
@@ -810,10 +817,26 @@ export const secretScanningV2ServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretScanningConfigActions.Read,
|
||||
ProjectPermissionSecretScanningConfigActions.Update,
|
||||
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(
|
||||
[
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { QueueWorkerProfile } from "@app/lib/types";
|
||||
|
||||
import { removeTrailingSlash } from "../fn";
|
||||
import { CustomLogger } from "../logger/logger";
|
||||
import { zpStr } from "../zod";
|
||||
@@ -57,6 +59,7 @@ const envSchema = z
|
||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
|
||||
QUEUE_WORKER_PROFILE: z.nativeEnum(QueueWorkerProfile).default(QueueWorkerProfile.All),
|
||||
HTTPS_ENABLED: zodStrBool,
|
||||
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
|
||||
// smtp options
|
||||
|
||||
@@ -78,3 +78,9 @@ export type OrgServiceActor = {
|
||||
authMethod: ActorAuthMethod;
|
||||
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";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueWorkerProfile } from "@app/lib/types";
|
||||
import {
|
||||
TFailedIntegrationSyncEmailsPayload,
|
||||
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 const queueServiceFactory = (
|
||||
redisUrl: string,
|
||||
@@ -325,7 +357,7 @@ export const queueServiceFactory = (
|
||||
});
|
||||
|
||||
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, {
|
||||
...queueSettings,
|
||||
connection
|
||||
@@ -344,6 +376,30 @@ export const queueServiceFactory = (
|
||||
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);
|
||||
queueContainerPg[jobName] = true;
|
||||
|
||||
@@ -363,7 +419,7 @@ export const queueServiceFactory = (
|
||||
listener: WorkerListener<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>[U]
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.QUEUE_WORKERS_ENABLED) {
|
||||
if (!appCfg.QUEUE_WORKERS_ENABLED || !isQueueEnabled(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
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"
|
||||
openapi: "PATCH /api/v2/secret-scanning/configs/{projectId}"
|
||||
openapi: "PATCH /api/v2/secret-scanning/configs"
|
||||
---
|
||||
|
||||
<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.
|
||||
|
||||
<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 team@infisical.com to purchase an enterprise license to use it.
|
||||
</Note>
|
||||
|
||||
## How Secret Scanning Works
|
||||
|
||||
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.
|
||||
|
||||
<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">
|
||||
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.
|
||||
@@ -38,19 +44,14 @@ Infisical supports GitHub App installation for creating a GitHub Radar Connectio
|
||||

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

|
||||

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

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

|
||||
</Step>
|
||||
<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 title="Authorize Connection">
|
||||
|
||||
@@ -314,3 +314,32 @@ Supports conditions and permission inversion
|
||||
| `create` | Create new SSH certificate templates |
|
||||
| `edit` | Modify SSH template configurations |
|
||||
| `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 |
|
||||
|
||||
@@ -102,7 +102,7 @@ export const GitHubDataSourceConfigFields = () => {
|
||||
isLoading={areRepositoriesLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
isMulti
|
||||
value={repositories?.find((project) => value.includes(project.name))}
|
||||
value={repositories?.filter((repository) => value.includes(repository.name))}
|
||||
onChange={(newValue) => {
|
||||
onChange(
|
||||
newValue
|
||||
|
||||
@@ -145,7 +145,7 @@ export const useUpdateSecretScanningConfig = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ projectId, ...params }: TSecretScanningConfig) => {
|
||||
const { data } = await apiRequest.patch<TGetSecretScanningConfigResponse>(
|
||||
`/api/v2/secret-scanning/configs/${projectId}`,
|
||||
`/api/v2/secret-scanning/configs?projectId=${projectId}`,
|
||||
params
|
||||
);
|
||||
|
||||
|
||||
@@ -237,7 +237,8 @@ export const useGetSecretScanningConfig = (
|
||||
queryKey: secretScanningV2Keys.configByProjectId(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetSecretScanningConfigResponse>(
|
||||
`/api/v2/secret-scanning/configs/${projectId}`
|
||||
"/api/v2/secret-scanning/configs",
|
||||
{ params: { projectId } }
|
||||
);
|
||||
|
||||
return data.config;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./DataSourceConfigDisplay";
|
||||
|
||||
@@ -9,7 +9,8 @@ import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
import { DataSourceConfigDisplay } from "@app/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay";
|
||||
|
||||
import { DataSourceConfigDisplay } from "./DataSourceConfigDisplay";
|
||||
|
||||
type Props = {
|
||||
dataSource: TSecretScanningDataSource;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
faBan,
|
||||
faCheck,
|
||||
faCopy,
|
||||
faEllipsisV,
|
||||
@@ -50,6 +51,13 @@ export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => {
|
||||
const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } =
|
||||
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 navigate = useNavigate();
|
||||
@@ -175,61 +183,76 @@ export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => {
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyId(id);
|
||||
}}
|
||||
>
|
||||
Copy Resource ID
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.TriggerScans}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faExpand} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTriggerScan();
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
position="left"
|
||||
sideOffset={42}
|
||||
content={`Manually trigger a scan for this ${resourceDetails.singularNoun}.`}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{!isActive && (
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content={`This ${resourceDetails.singularNoun} will not be scanned due to exclusion in Data Source configuration.`}
|
||||
>
|
||||
<div className="ml-auto">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
<span>Inactive</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyId(id);
|
||||
}}
|
||||
>
|
||||
Copy Resource ID
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.TriggerScans}
|
||||
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">
|
||||
<span> Trigger Scan</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-bunker-300"
|
||||
size="sm"
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
position="left"
|
||||
sideOffset={42}
|
||||
content={`Manually trigger a scan for this ${resourceDetails.singularNoun}.`}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-between gap-1">
|
||||
<span> Trigger Scan</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-bunker-300"
|
||||
size="sm"
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faBullseye,
|
||||
faCheckCircle,
|
||||
faFilter,
|
||||
faInfoCircle,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -10,6 +14,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
@@ -20,6 +29,7 @@ import {
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
@@ -45,6 +55,15 @@ type Props = {
|
||||
dataSource: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
enum ResourceStatus {
|
||||
Active = "active",
|
||||
Inactive = "inactive"
|
||||
}
|
||||
|
||||
type ResourceFilters = {
|
||||
status: ResourceStatus[];
|
||||
};
|
||||
|
||||
export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
@@ -53,6 +72,10 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
||||
ProjectPermissionSub.SecretScanningDataSources
|
||||
);
|
||||
|
||||
const [filters, setFilters] = useState<ResourceFilters>({
|
||||
status: [ResourceStatus.Active]
|
||||
});
|
||||
|
||||
const { data: resources = [], isPending: isResourcesPending } = useListSecretScanningResources(
|
||||
{ dataSourceId: dataSource.id, type: dataSource.type },
|
||||
{
|
||||
@@ -81,6 +104,19 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
||||
resources
|
||||
.filter((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();
|
||||
|
||||
@@ -114,7 +150,7 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
||||
return resourceOne.name.toLowerCase().localeCompare(resourceTwo.name.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[resources, orderDirection, search, orderBy]
|
||||
[resources, orderDirection, search, orderBy, filters, dataSource]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
@@ -141,15 +177,74 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
||||
|
||||
const resourceDetails = RESOURCE_DESCRIPTION_HELPER[dataSource.type];
|
||||
|
||||
const isTableFiltered = Boolean(filters.status.length);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder={`Search ${resourceDetails.pluralNoun}...`}
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
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">
|
||||
<Table>
|
||||
<THead>
|
||||
|
||||
@@ -24,10 +24,11 @@ export const SecretScanningDataSourcesSection = () => {
|
||||
|
||||
const { data: dataSources = [], isPending: isDataSourcesPending } =
|
||||
useListSecretScanningDataSources(currentWorkspace.id, {
|
||||
refetchInterval: 30000
|
||||
refetchInterval: 30000,
|
||||
enabled: subscription.secretScanning
|
||||
});
|
||||
|
||||
if (isDataSourcesPending)
|
||||
if (subscription.secretScanning && isDataSourcesPending)
|
||||
return (
|
||||
<div className="flex h-[60vh] flex-col items-center justify-center gap-2">
|
||||
<Spinner />
|
||||
@@ -57,7 +58,7 @@ export const SecretScanningDataSourcesSection = () => {
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
|
||||
@@ -45,8 +45,6 @@ import {
|
||||
|
||||
import { SecretScanningDataSourceRow } from "./SecretScanningDataSourceRow";
|
||||
|
||||
// import { getSecretSyncDestinationColValues } from "./helpers";
|
||||
|
||||
enum DataSourcesOrderBy {
|
||||
Findings = "findings",
|
||||
Name = "name",
|
||||
|
||||
@@ -64,7 +64,7 @@ export const DeleteProjectSection = () => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
to: `/organization/${ProjectType.SecretScanning}/overview` as const
|
||||
});
|
||||
handlePopUpClose("deleteWorkspace");
|
||||
} catch (err) {
|
||||
@@ -123,7 +123,7 @@ export const DeleteProjectSection = () => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
to: `/organization/${ProjectType.SecretScanning}/overview` as const
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { ProjectSshConfigCasSection } from "./components";
|
||||
|
||||
export const ProjectSshTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectSshConfigCasSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 +0,0 @@
|
||||
export { ProjectSshConfigCasSection } from "./ProjectSshConfigCasSection";
|
||||
@@ -1 +0,0 @@
|
||||
export { ProjectSshTab } from "./ProjectSshTab";
|
||||
Reference in New Issue
Block a user