improvements: address feedback and setup queue worker profiles

This commit is contained in:
Scott Wilson
2025-06-02 14:40:06 -07:00
parent 3ae2ec1f51
commit d924580599
31 changed files with 356 additions and 254 deletions

View File

@@ -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;

View File

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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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"]
);
}

View File

@@ -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(
[
{

View File

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

View File

@@ -78,3 +78,9 @@ export type OrgServiceActor = {
authMethod: ActorAuthMethod;
orgId: string;
};
export enum QueueWorkerProfile {
All = "all",
Standard = "standard",
SecretScanning = "secret-scanning"
}

View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
---
title: "Get by Project ID"
openapi: "GET /api/v2/secret-scanning/configs/{projectId}"
openapi: "GET /api/v2/secret-scanning/configs"
---

View File

@@ -1,6 +1,6 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-scanning/configs/{projectId}"
openapi: "PATCH /api/v2/secret-scanning/configs"
---
<Note>

View File

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

View File

@@ -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
![github radar app webhook](/images/app-connections/github-radar/self-hosted-github-radar-step-3.png)
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`
![github radar app permissions 1](/images/app-connections/github-radar/self-hosted-github-radar-step-4.png)
![github radar app permissions 2](/images/app-connections/github-radar/self-hosted-github-radar-step-5.png)
Subscribe to the following events:
1. **Check run**
2. **Pull request**
3. **Push**
- **Push**
![github radar app events](/images/app-connections/github-radar/self-hosted-github-radar-step-6.png)
@@ -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
![App Connections Tab](/images/app-connections/general/add-connection.png)
</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.
![Select GitHub Radar Connection](/images/app-connections/github-radar/select-github-radar-connection.png)
</Step>
<Step title="Authorize Connection">

View File

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

View File

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

View File

@@ -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
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);

View File

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

View File

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

View File

@@ -45,8 +45,6 @@ import {
import { SecretScanningDataSourceRow } from "./SecretScanningDataSourceRow";
// import { getSecretSyncDestinationColValues } from "./helpers";
enum DataSourcesOrderBy {
Findings = "findings",
Name = "name",

View File

@@ -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);

View File

@@ -1,9 +0,0 @@
import { ProjectSshConfigCasSection } from "./components";
export const ProjectSshTab = () => {
return (
<div>
<ProjectSshConfigCasSection />
</div>
);
};

View File

@@ -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>
);
};

View File

@@ -1 +0,0 @@
export { ProjectSshConfigCasSection } from "./ProjectSshConfigCasSection";

View File

@@ -1 +0,0 @@
export { ProjectSshTab } from "./ProjectSshTab";