feat(secret-scanning): BitBucket data source

This commit is contained in:
x032205
2025-07-03 00:01:37 -04:00
parent ae35a863bc
commit edecfb1f62
30 changed files with 796 additions and 54 deletions
@@ -0,0 +1,122 @@
import { useEffect } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { MultiValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import {
TBitBucketRepo,
useBitBucketConnectionListRepositories
} from "@app/hooks/api/appConnections/bitbucket";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
enum ScanMethod {
AllRepositories = "all-repositories",
SelectRepositories = "select-repositories"
}
export const BitBucketDataSourceConfigFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.BitBucket;
}
>();
const connectionId = useWatch({ control, name: "connection.id" });
const isUpdate = Boolean(watch("id"));
const { data: repositories, isPending: areRepositoriesLoading } =
useBitBucketConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) });
const includeRepos = watch("config.includeRepos");
const scanMethod =
!includeRepos || includeRepos[0] === "*"
? ScanMethod.AllRepositories
: ScanMethod.SelectRepositories;
useEffect(() => {
if (!includeRepos) {
setValue("config.includeRepos", ["*"]);
}
}, [includeRepos, setValue]);
return (
<>
<SecretScanningDataSourceConnectionField
isUpdate={isUpdate}
onChange={() => {
if (scanMethod === ScanMethod.SelectRepositories) {
setValue("config.includeRepos", []);
}
}}
/>
<FormControl label="Scan Repositories">
<Select
value={scanMethod}
onValueChange={(val) => {
setValue("config.includeRepos", val === ScanMethod.AllRepositories ? ["*"] : []);
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
isDisabled={!connectionId}
>
{Object.values(ScanMethod).map((method) => {
return (
<SelectItem className="capitalize" value={method} key={method}>
{method.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</FormControl>
{scanMethod === ScanMethod.SelectRepositories && (
<Controller
name="config.includeRepos"
defaultValue={["*"]}
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Include Repositories"
helperText={
<Tooltip
className="max-w-md"
content={<>Ensure that your connection has the correct permissions.</>}
>
<div>
<span>Don&#39;t see the repository you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={areRepositoriesLoading && Boolean(connectionId)}
isDisabled={!connectionId}
isMulti
value={repositories?.filter((repository) => value.includes(repository.name))}
onChange={(newValue) => {
onChange(
newValue ? (newValue as MultiValue<TBitBucketRepo>).map((p) => p.name) : null
);
}}
options={repositories}
placeholder="Select repositories..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
)}
</>
);
};
@@ -5,10 +5,12 @@ import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { BitBucketDataSourceConfigFields } from "./BitBucketDataSourceConfigFields";
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields,
[SecretScanningDataSource.BitBucket]: BitBucketDataSourceConfigFields
};
export const SecretScanningDataSourceConfigFields = () => {
@@ -0,0 +1,27 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { SecretScanningDataSourceConfigReviewSection } from "./shared";
export const BitBucketDataSourceReviewFields = () => {
const { watch } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.BitBucket;
}
>();
const [{ includeRepos }, connection] = watch(["config", "connection"]);
const shouldScanAll = includeRepos[0] === "*";
return (
<SecretScanningDataSourceConfigReviewSection>
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
<GenericFieldLabel label="Scan Repositories">
{shouldScanAll ? "All" : includeRepos.join(", ")}
</GenericFieldLabel>
</SecretScanningDataSourceConfigReviewSection>
);
};
@@ -4,10 +4,12 @@ import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { BitBucketDataSourceReviewFields } from "./BitBucketDataSourceReviewFields";
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields,
[SecretScanningDataSource.BitBucket]: BitBucketDataSourceReviewFields
};
export const SecretScanningDataSourceReviewFields = () => {
@@ -0,0 +1,18 @@
import { z } from "zod";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema";
export const BitBucketDataSourceSchema = z
.object({
type: z.literal(SecretScanningDataSource.BitBucket),
config: z.object({
includeRepos: z
.string()
.array()
.min(1, "One or more repositories required")
.max(100, "Cannot configure more than 100 repositories")
})
})
.merge(BaseSecretScanningDataSourceSchema({ isConnectionRequired: true }));
@@ -1,9 +1,11 @@
import { z } from "zod";
import { BitBucketDataSourceSchema } from "./bitbucket-data-source-schema";
import { GitHubDataSourceSchema } from "./github-data-source-schema";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema
GitHubDataSourceSchema,
BitBucketDataSourceSchema
]);
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
+14 -1
View File
@@ -19,6 +19,11 @@ export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
name: "GitHub",
image: "GitHub.png",
size: 45
},
[SecretScanningDataSource.BitBucket]: {
name: "BitBucket",
image: "BitBucket.png",
size: 45
}
};
@@ -26,7 +31,8 @@ export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<
SecretScanningDataSource,
AppConnection
> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
[SecretScanningDataSource.BitBucket]: AppConnection.BitBucket
};
export const RESOURCE_DESCRIPTION_HELPER: Record<
@@ -45,6 +51,13 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
singularNoun: "repository",
pluralTitle: "Repositories",
singularTitle: "Repository"
},
[SecretScanningDataSource.BitBucket]: {
verb: "push",
pluralNoun: "repositories",
singularNoun: "repository",
pluralTitle: "Repositories",
singularTitle: "Repository"
}
};
@@ -1,5 +1,6 @@
export enum SecretScanningDataSource {
GitHub = "github"
GitHub = "github",
BitBucket = "bitbucket"
}
export enum SecretScanningScanStatus {
@@ -0,0 +1,17 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretScanningDataSource } from "../enums";
import { TSecretScanningDataSourceBase } from "./shared";
export type TBitBucketDataSource = TSecretScanningDataSourceBase & {
type: SecretScanningDataSource.BitBucket;
config: {
includeRepos: string[];
};
};
export type TBitBucketDataSourceOption = {
name: string;
type: SecretScanningDataSource.BitBucket;
connection: AppConnection.BitBucket;
};
@@ -8,9 +8,10 @@ import {
SecretScanningScanStatus,
SecretScanningScanType
} from "../enums";
import { TBitBucketDataSource, TBitBucketDataSourceOption } from "./bitbucket-data-source";
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
export type TSecretScanningDataSource = TGitHubDataSource;
export type TSecretScanningDataSource = TGitHubDataSource | TBitBucketDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt: string | null;
@@ -23,7 +24,7 @@ export type TListSecretScanningDataSources = {
dataSources: TSecretScanningDataSourceWithDetails[];
};
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption;
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption | TBitBucketDataSourceOption;
export type TListSecretScanningDataSourceOptions = {
dataSourceOptions: TSecretScanningDataSourceOption[];
+19 -23
View File
@@ -1,41 +1,37 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute } from "@tanstack/react-router";
import { BreadcrumbTypes } from '@app/components/v2'
import { workspaceKeys } from '@app/hooks/api'
import {
fetchUserProjectPermissions,
roleQueryKeys,
} from '@app/hooks/api/roles/queries'
import { fetchWorkspaceById } from '@app/hooks/api/workspace/queries'
import { ProjectLayout } from '@app/layouts/ProjectLayout'
import { ProjectSelect } from '@app/layouts/ProjectLayout/components/ProjectSelect'
import { BreadcrumbTypes } from "@app/components/v2";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout',
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId),
})
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId,
workspaceId: params.projectId
}),
queryFn: () =>
fetchUserProjectPermissions({ workspaceId: params.projectId }),
})
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
project,
breadcrumbs: [
{
type: BreadcrumbTypes.Component,
component: ProjectSelect,
},
],
}
},
})
component: ProjectSelect
}
]
};
}
});
@@ -1,14 +1,14 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { createFileRoute, redirect } from "@tanstack/react-router";
// this is done as part of migration for multi product inside project
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval',
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval"
)({
beforeLoad: ({ params, search }) => {
throw redirect({
to: '/projects/$projectId/secret-manager/approval',
to: "/projects/$projectId/secret-manager/approval",
params,
search,
})
},
})
search
});
}
});
@@ -0,0 +1,18 @@
import { GenericFieldLabel } from "@app/components/v2";
import { TBitBucketDataSource } from "@app/hooks/api/secretScanningV2/types/bitbucket-data-source";
type Props = {
dataSource: TBitBucketDataSource;
};
export const BitBucketDataSourceConfigDisplay = ({ dataSource }: Props) => {
const {
config: { includeRepos }
} = dataSource;
return (
<GenericFieldLabel label="Scan Repositories">
{includeRepos.includes("*") ? "All" : includeRepos.join(", ")}
</GenericFieldLabel>
);
};
@@ -4,6 +4,7 @@ import {
} from "@app/hooks/api/secretScanningV2";
import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay";
import { BitBucketDataSourceConfigDisplay } from "./BitBucketDataSourceConfigDisplay";
type Props = {
dataSource: TSecretScanningDataSource;
@@ -13,6 +14,8 @@ export const DataSourceConfigDisplay = ({ dataSource }: Props) => {
switch (dataSource.type) {
case SecretScanningDataSource.GitHub:
return <GitHubDataSourceConfigDisplay dataSource={dataSource} />;
case SecretScanningDataSource.BitBucket:
return <BitBucketDataSourceConfigDisplay dataSource={dataSource} />;
default:
throw new Error(
`Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}`