Merge branch 'heads/main' into daniel/gateway-auth-methods

This commit is contained in:
Daniel Hougaard
2025-06-05 04:49:35 +04:00
400 changed files with 15599 additions and 1473 deletions
@@ -41,7 +41,7 @@ export const DeleteProjectTemplateModal = ({ isOpen, onOpenChange, template }: P
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey="confirm"
onDeleteApproved={handleDeleteProjectTemplate}
/>
@@ -112,7 +112,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
/>
<DeleteActionModal
isOpen={popUp.removeTemplate.isOpen}
title={`Are you sure want to delete ${projectTemplate.name}?`}
title={`Are you sure you want to delete ${projectTemplate.name}?`}
deleteKey="confirm"
onChange={(isOpen) => handlePopUpToggle("removeTemplate", isOpen)}
onDeleteApproved={handleRemoveTemplate}
@@ -66,7 +66,7 @@ export const DeleteSecretRotationV2Modal = ({
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteSecretRotation}
>
@@ -0,0 +1,97 @@
import { useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Modal, ModalContent } from "@app/components/v2";
import {
SecretScanningDataSource,
TSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import { SecretScanningDataSourceForm } from "./forms";
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
import { SecretScanningDataSourceSelect } from "./SecretScanningDataSourceSelect";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
selectedDataSource: SecretScanningDataSource | null;
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
};
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
if (selectedDataSource) {
return (
<SecretScanningDataSourceForm
onCancel={() => setSelectedDataSource(null)}
type={selectedDataSource}
{...props}
/>
);
}
return <SecretScanningDataSourceSelect onSelect={setSelectedDataSource} />;
};
export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...props }: Props) => {
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
null
);
return (
<Modal
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) setSelectedDataSource(null);
onOpenChange(open);
}}
>
<ModalContent
title={
selectedDataSource ? (
<SecretScanningDataSourceModalHeader isConfigured={false} type={selectedDataSource} />
) : (
<div className="flex items-center text-mineshaft-300">
Add Data Source
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
)
}
onPointerDownOutside={(e) => e.preventDefault()}
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
subTitle={
selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
}
bodyClassName="overflow-visible"
>
<Content
onComplete={() => {
setSelectedDataSource(null);
onOpenChange(false);
}}
selectedDataSource={selectedDataSource}
setSelectedDataSource={setSelectedDataSource}
{...props}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,66 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
TSecretScanningDataSource,
useDeleteSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
type Props = {
dataSource?: TSecretScanningDataSource;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onComplete?: () => void;
};
export const DeleteSecretScanningDataSourceModal = ({
isOpen,
onOpenChange,
dataSource,
onComplete
}: Props) => {
const deleteDataSource = useDeleteSecretScanningDataSource();
if (!dataSource) return null;
const { id: dataSourceId, name, type, projectId } = dataSource;
const handleDeleteDataSource = async () => {
const dataSourceType = SECRET_SCANNING_DATA_SOURCE_MAP[type].name;
try {
await deleteDataSource.mutateAsync({
dataSourceId,
type,
projectId
});
createNotification({
text: `Successfully deleted ${dataSourceType} Data Source`,
type: "success"
});
if (onComplete) onComplete();
onOpenChange(false);
} catch {
createNotification({
text: `Failed to delete ${dataSourceType} Data Source`,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteDataSource}
>
<p className="mt-1 font-inter text-sm text-mineshaft-400">
Findings associated with this data source will be preserved.
</p>
</DeleteActionModal>
);
};
@@ -0,0 +1,36 @@
import { Modal, ModalContent } from "@app/components/v2";
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { SecretScanningDataSourceForm } from "./forms";
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
dataSource?: TSecretScanningDataSource;
};
export const EditSecretScanningDataSourceModal = ({
dataSource,
onOpenChange,
...props
}: Props) => {
if (!dataSource) return null;
return (
<Modal {...props} onOpenChange={onOpenChange}>
<ModalContent
title={<SecretScanningDataSourceModalHeader isConfigured type={dataSource.type} />}
className="max-w-2xl"
bodyClassName="overflow-visible"
>
<SecretScanningDataSourceForm
onComplete={() => onOpenChange(false)}
onCancel={() => onOpenChange(false)}
dataSource={dataSource}
type={dataSource.type}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,47 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
type Props = {
type: SecretScanningDataSource;
isConfigured: boolean;
};
export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Props) => {
const dataSourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
return (
<div className="flex w-full items-start gap-2">
<img
alt={`${dataSourceDetails.name} logo`}
src={`/images/integrations/${dataSourceDetails.image}`}
className="h-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
{dataSourceDetails.name} Data Source
<a
target="_blank"
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source
</p>
</div>
</div>
);
};
@@ -0,0 +1,91 @@
import { faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Spinner, Tooltip } from "@app/components/v2";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
SecretScanningDataSource,
useSecretScanningDataSourceOptions
} from "@app/hooks/api/secretScanningV2";
type Props = {
onSelect: (type: SecretScanningDataSource) => void;
};
export const SecretScanningDataSourceSelect = ({ onSelect }: Props) => {
const { isPending, data: dataSourceOptions } = useSecretScanningDataSourceOptions();
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
</div>
);
}
return (
<div className="grid grid-cols-3 gap-2">
{dataSourceOptions?.map(({ type }) => {
const { image, name, size } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
return (
<button
type="button"
key={type}
onClick={() => onSelect(type)}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
width={size}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more services.</p>
<p>
{`If you don't see the third-party
service you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4 hover:bg-mineshaft-900/50">
<FontAwesomeIcon className="mt-auto text-3xl" icon={faWrench} />
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
Coming Soon
</div>
</div>
</Tooltip>
</div>
);
};
@@ -0,0 +1,90 @@
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatDistance } from "date-fns";
import { twMerge } from "tailwind-merge";
import { Badge, Tooltip } from "@app/components/v2";
import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
type Props = {
status: SecretScanningScanStatus;
statusMessage?: string | null;
className?: string;
scannedAt?: string | null;
};
export const SecretScanningScanStatusBadge = ({
status,
statusMessage,
className,
scannedAt
}: Props) => {
if (status === SecretScanningScanStatus.Failed) {
let errorMessage = statusMessage;
if (statusMessage) {
try {
errorMessage = JSON.stringify(JSON.parse(statusMessage), null, 2);
} catch {
errorMessage = statusMessage;
}
}
return (
<Tooltip
position="left"
className="max-w-sm select-text"
content={
<div className="flex flex-col gap-2 whitespace-normal py-1">
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<div className="text-xs">Failure Reason</div>
</div>
<div className="break-words rounded bg-mineshaft-600 p-2 text-xs">{errorMessage}</div>
{scannedAt && (
<div className="mt-1 text-xs text-mineshaft-400">
Attempted {formatDistance(new Date(scannedAt), new Date(), { addSuffix: true })}
</div>
)}
</div>
</div>
}
>
<div>
<Badge
variant="danger"
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
>
<FontAwesomeIcon icon={faXmark} />
Scan Error
</Badge>
</div>
</Tooltip>
);
}
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
return (
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant="primary"
>
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
<span>Scanning</span>
</Badge>
);
}
return (
<Badge
variant="success"
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faCheck} />
<span>Complete</span>
</Badge>
);
};
@@ -0,0 +1,126 @@
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 {
TGitHubRadarConnectionRepository,
useGitHubRadarConnectionListRepositories
} from "@app/hooks/api/appConnections/github-radar";
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 GitHubDataSourceConfigFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitHub;
}
>();
const connectionId = useWatch({ control, name: "connection.id" });
const isUpdate = Boolean(watch("id"));
const { data: repositories, isPending: areRepositoriesLoading } =
useGitHubRadarConnectionListRepositories(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<TGitHubRadarConnectionRepository>).map(
(p) => p.name
)
: null
);
}}
options={repositories}
placeholder="Select repositories..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.name}
/>
</FormControl>
)}
/>
)}
</>
);
};
@@ -0,0 +1,55 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Switch } from "@app/components/v2";
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields
};
export const SecretScanningDataSourceConfigFields = () => {
const { watch, control } = useFormContext<TSecretScanningDataSourceForm>();
const type = watch("type");
const Component = COMPONENT_MAP[type];
const autoScanDescription = RESOURCE_DESCRIPTION_HELPER[type];
return (
<>
<p className="mb-4 text-sm text-bunker-300">Connect and configure your Data Source.</p>
<Component />
<Controller
control={control}
name="isAutoScanEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl
helperText={
value
? `Scans will automatically be triggered when a ${autoScanDescription.verb} occurs to ${autoScanDescription.pluralNoun} associated with this data source.`
: "Manually trigger scans to detect secret leaks."
}
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="auto-scan-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p className="w-[9.6rem]">Auto-Scan {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
);
}}
/>
</>
);
};
@@ -0,0 +1 @@
export * from "./SecretScanningDataSourceConfigFields";
@@ -0,0 +1,103 @@
import { Controller, useFormContext } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { TSecretScanningDataSourceForm } from "./schemas";
type Props = {
onChange?: VoidFunction;
isUpdate?: boolean;
};
export const SecretScanningDataSourceConnectionField = ({
onChange: callback,
isUpdate
}: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretScanningDataSourceForm>();
const dataSourceType = watch("type");
const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
);
return (
<>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="App Connections can be created from the Organization Settings page."
isError={Boolean(error)}
errorText={error?.message}
label={`${connectionName} Connection`}
helperText={
isUpdate ? (
"Cannot be updated"
) : (
<p>
Check out{" "}
<a
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
target="_blank"
className="underline"
rel="noopener noreferrer"
>
our docs
</a>{" "}
to ensure your connection has the required permissions for secret scanning.
</p>
)
}
>
<FilterableSelect
value={value}
onChange={(newValue) => {
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
isDisabled={isUpdate}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {connectionName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${connectionName} Connections. Contact an admin to create one.`
)}
</p>
)}
</>
);
};
@@ -0,0 +1,51 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input, TextArea } from "@app/components/v2";
import { TSecretScanningDataSourceForm } from "./schemas";
export const SecretScanningDataSourceDetailsFields = () => {
const { control } = useFormContext<TSecretScanningDataSourceForm>();
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Provide a name and description for this Data Source.
</p>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
helperText="Must be slug-friendly"
isError={Boolean(error)}
errorText={error?.message}
label="Name"
>
<Input autoFocus value={value} onChange={onChange} placeholder="my-data-source" />
</FormControl>
)}
control={control}
name="name"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Description"
>
<TextArea
value={value ?? ""}
onChange={onChange}
placeholder="Provide a description for this data source..."
className="!resize-none"
rows={4}
/>
</FormControl>
)}
control={control}
name="description"
/>
</>
);
};
@@ -0,0 +1,180 @@
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
SecretScanningDataSource,
TSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import {
useCreateSecretScanningDataSource,
useUpdateSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2/mutations";
import { SecretScanningDataSourceSchema, TSecretScanningDataSourceForm } from "./schemas";
import { SecretScanningDataSourceConfigFields } from "./SecretScanningDataSourceConfigFields";
import { SecretScanningDataSourceDetailsFields } from "./SecretScanningDataSourceDetailsFields";
import { SecretScanningDataSourceReviewFields } from "./SecretScanningDataSourceReviewFields";
type Props = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
type: SecretScanningDataSource;
onCancel: () => void;
dataSource?: TSecretScanningDataSource;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningDataSourceForm)[] }[] =
[
{ name: "Configuration", key: "config", fields: ["config", "isAutoScanEnabled", "connection"] },
{ name: "Details", key: "details", fields: ["name", "description"] },
{ name: "Review", key: "review", fields: [] }
];
export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataSource }: Props) => {
const createDataSource = useCreateSecretScanningDataSource();
const updateDataSource = useUpdateSecretScanningDataSource();
const { currentWorkspace } = useWorkspace();
const { name: sourceType } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const formMethods = useForm<TSecretScanningDataSourceForm>({
resolver: zodResolver(SecretScanningDataSourceSchema),
defaultValues: dataSource ?? {
type,
isAutoScanEnabled: true // scott: this may need to be derived from type in the future
},
reValidateMode: "onChange"
});
const onSubmit = async ({ connection, ...formData }: TSecretScanningDataSourceForm) => {
const mutation = dataSource
? updateDataSource.mutateAsync({
dataSourceId: dataSource.id,
projectId: dataSource.projectId,
...formData
})
: createDataSource.mutateAsync({
...formData,
connectionId: connection?.id,
projectId: currentWorkspace.id
});
try {
const source = await mutation;
createNotification({
text: `Successfully ${source ? "updated" : "created"} ${sourceType} Data Source`,
type: "success"
});
onComplete(source);
} catch (err: any) {
createNotification({
title: `Failed to ${dataSource ? "update" : "create"} ${sourceType} Data Source`,
text: err.message,
type: "error"
});
}
};
const handlePrev = () => {
if (selectedTabIndex === 0) {
onCancel();
return;
}
setSelectedTabIndex((prev) => prev - 1);
};
const {
handleSubmit,
trigger,
formState: { isSubmitting }
} = formMethods;
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
const handleNext = async () => {
if (isFinalStep) {
handleSubmit(onSubmit)();
return;
}
const isValid = await isStepValid(selectedTabIndex);
if (!isValid) return;
setSelectedTabIndex((prev) => prev + 1);
};
const isTabEnabled = async (index: number) => {
let isEnabled = true;
for (let i = index - 1; i >= 0; i -= 1) {
// eslint-disable-next-line no-await-in-loop
isEnabled = isEnabled && (await isStepValid(i));
}
return isEnabled;
};
return (
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
<FormProvider {...formMethods}>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
{FORM_TABS.map((tab, index) => (
<Tab
onClick={async (e) => {
e.preventDefault();
const isEnabled = await isTabEnabled(index);
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
}}
className={({ selected }) =>
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
key={tab.key}
>
{index + 1}. {tab.name}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<SecretScanningDataSourceConfigFields />
</Tab.Panel>
<Tab.Panel>
<SecretScanningDataSourceDetailsFields />
</Tab.Panel>
<Tab.Panel>
<SecretScanningDataSourceReviewFields />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button
onClick={handleNext}
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema="secondary"
>
{isFinalStep ? `${dataSource ? "Update" : "Create"} Data Source` : "Next"}
</Button>
<Button onClick={handlePrev} colorSchema="secondary">
Back
</Button>
</div>
</form>
);
};
@@ -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 GitHubDataSourceReviewFields = () => {
const { watch } = useFormContext<
TSecretScanningDataSourceForm & {
type: SecretScanningDataSource.GitHub;
}
>();
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>
);
};
@@ -0,0 +1,34 @@
import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
import { TSecretScanningDataSourceForm } from "../schemas";
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields
};
export const SecretScanningDataSourceReviewFields = () => {
const { watch } = useFormContext<TSecretScanningDataSourceForm>();
const { type, name, description } = watch();
const Component = COMPONENT_MAP[type];
return (
<div className="mb-4 flex flex-col gap-6">
<Component />
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Details</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./SecretScanningDataSourceReviewFields";
@@ -0,0 +1,16 @@
import { ReactNode } from "react";
type Props = {
children: ReactNode;
};
export const SecretScanningDataSourceConfigReviewSection = ({ children }: Props) => {
return (
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Configuration</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">{children}</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./SecretScanningDataSourceConfigReviewSection";
@@ -0,0 +1 @@
export * from "./SecretScanningDataSourceForm";
@@ -0,0 +1,18 @@
import { z } from "zod";
import { slugSchema } from "@app/lib/schemas";
type SchemaOptions = {
isConnectionRequired: boolean;
};
export const BaseSecretScanningDataSourceSchema = ({ isConnectionRequired }: SchemaOptions) =>
z.object({
name: slugSchema({ field: "Name" }),
description: z.string().trim().max(256, "Cannot exceed 256 characters").nullish(),
connection: isConnectionRequired
? z.object({ name: z.string(), id: z.string().uuid() })
: z.null().or(z.undefined()),
isAutoScanEnabled: z.boolean(),
id: z.string().uuid().optional()
});
@@ -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 GitHubDataSourceSchema = z
.object({
type: z.literal(SecretScanningDataSource.GitHub),
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 }));
@@ -0,0 +1,9 @@
import { z } from "zod";
import { GitHubDataSourceSchema } from "./github-data-source-schema";
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
GitHubDataSourceSchema
]);
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
@@ -0,0 +1,3 @@
export * from "./CreateSecretScanningDataSourceModal";
export * from "./EditSecretScanningDataSourceModal";
export * from "./SecretScanningScanStatus";
@@ -52,7 +52,7 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteSecretSync}
>
+6 -2
View File
@@ -4,6 +4,7 @@ import { twMerge } from "tailwind-merge";
interface IProps {
children: React.ReactNode;
className?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
const badgeVariants = cva(
@@ -23,9 +24,12 @@ const badgeVariants = cva(
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
export const Badge = ({ children, className, variant }: BadgeProps) => {
export const Badge = ({ children, className, variant, ...props }: BadgeProps) => {
return (
<div className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}>
<div
className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}
{...props}
>
{children}
</div>
);
+3 -1
View File
@@ -18,6 +18,7 @@ type Props = {
isDisabled?: boolean;
icon?: IconProp;
isMulti?: boolean;
iconClassName?: string;
};
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
@@ -33,6 +34,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
dropdownContainerClassName,
position,
containerClassName,
iconClassName,
...props
},
ref
@@ -58,7 +60,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
)}
>
<div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
{props.icon && <FontAwesomeIcon icon={props.icon} />}
{props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />}
<div className="flex-1 truncate">
<SelectPrimitive.Value placeholder={placeholder} />
</div>
+10 -4
View File
@@ -27,10 +27,6 @@ export const ROUTE_PATHS = Object.freeze({
"/organization/sso",
"/_authenticate/_inject-org-details/_org-layout/organization/sso"
),
SecretScanning: setRoute(
"/organization/secret-scanning",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
),
SecretSharing: setRoute(
"/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
@@ -322,6 +318,16 @@ export const ROUTE_PATHS = Object.freeze({
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
)
},
SecretScanning: {
DataSourceByIdPage: setRoute(
"/secret-scanning/$projectId/data-sources/$type/$dataSourceId",
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
),
FindingsPage: setRoute(
"/secret-scanning/$projectId/findings",
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/findings"
)
},
Public: {
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
ViewSecretRequestByIDPage: setRoute(
@@ -122,6 +122,26 @@ export enum ProjectPermissionSecretRotationActions {
RotateSecrets = "rotate-secrets"
}
export enum ProjectPermissionSecretScanningDataSourceActions {
Read = "read-data-sources",
Create = "create-data-sources",
Edit = "edit-data-sources",
Delete = "delete-data-sources",
TriggerScans = "trigger-data-source-scans",
ReadScans = "read-data-source-scans",
ReadResources = "read-data-source-resources"
}
export enum ProjectPermissionSecretScanningFindingActions {
Read = "read-findings",
Update = "update-findings"
}
export enum ProjectPermissionSecretScanningConfigActions {
Read = "read-configs",
Update = "update-configs"
}
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
@@ -208,7 +228,10 @@ export enum ProjectPermissionSub {
Kms = "kms",
Cmek = "cmek",
SecretSyncs = "secret-syncs",
Kmip = "kmip"
Kmip = "kmip",
SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs"
}
export type SecretSubjectFields = {
@@ -343,6 +366,12 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip];
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [
ProjectPermissionSecretScanningDataSourceActions,
ProjectPermissionSub.SecretScanningDataSources
]
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
+10 -2
View File
@@ -1,5 +1,6 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { faGithub, IconDefinition } from "@fortawesome/free-brands-svg-icons";
import {
faBullseye,
faKey,
faLink,
faLock,
@@ -19,6 +20,7 @@ import {
DatabricksConnectionMethod,
GcpConnectionMethod,
GitHubConnectionMethod,
GitHubRadarConnectionMethod,
HCVaultConnectionMethod,
HumanitecConnectionMethod,
LdapConnectionMethod,
@@ -36,10 +38,15 @@ import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-con
export const APP_CONNECTION_MAP: Record<
AppConnection,
{ name: string; image: string; size?: number; enterprise?: boolean }
{ name: string; image: string; size?: number; icon?: IconDefinition; enterprise?: boolean }
> = {
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" },
[AppConnection.GitHubRadar]: {
name: "GitHub Radar",
image: "GitHub.png",
icon: faBullseye
},
[AppConnection.GCP]: {
name: "GCP",
image: "Google Cloud Platform.png"
@@ -73,6 +80,7 @@ export const APP_CONNECTION_MAP: Record<
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
switch (method) {
case GitHubConnectionMethod.App:
case GitHubRadarConnectionMethod.App:
return { name: "GitHub App", icon: faGithub };
case AzureKeyVaultConnectionMethod.OAuth:
case AzureAppConfigurationConnectionMethod.OAuth:
+9 -4
View File
@@ -61,10 +61,14 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
return project;
};
export const getProjectHomePage = (workspace: Workspace) => {
if (workspace.type === ProjectType.CertificateManager) {
return `/${workspace.type}/$projectId/subscribers` as const;
switch (workspace.type) {
case ProjectType.CertificateManager:
return `/${workspace.type}/$projectId/subscribers` as const;
case ProjectType.SecretScanning:
return `/${workspace.type}/$projectId/data-sources` as const;
default:
return `/${workspace.type}/$projectId/overview` as const;
}
return `/${workspace.type}/$projectId/overview` as const;
};
export const getProjectTitle = (type: ProjectType) => {
@@ -72,7 +76,8 @@ export const getProjectTitle = (type: ProjectType) => {
[ProjectType.SecretManager]: "Secret Management",
[ProjectType.KMS]: "Key Management",
[ProjectType.CertificateManager]: "Cert Management",
[ProjectType.SSH]: "SSH"
[ProjectType.SSH]: "SSH",
[ProjectType.SecretScanning]: "Secret Scanning"
};
return titleConvert[type];
};
+59
View File
@@ -0,0 +1,59 @@
import {
faBan,
faCheck,
faMagnifyingGlassMinus,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
SecretScanningDataSource,
SecretScanningFindingStatus
} from "@app/hooks/api/secretScanningV2";
export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
SecretScanningDataSource,
{ name: string; image: string; size: number }
> = {
[SecretScanningDataSource.GitHub]: {
name: "GitHub",
image: "GitHub.png",
size: 45
}
};
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<
SecretScanningDataSource,
AppConnection
> = {
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar
};
export const RESOURCE_DESCRIPTION_HELPER: Record<
SecretScanningDataSource,
{
verb: string;
pluralNoun: string;
singularNoun: string;
singularTitle: string;
pluralTitle: string;
}
> = {
[SecretScanningDataSource.GitHub]: {
verb: "push",
pluralNoun: "repositories",
singularNoun: "repository",
pluralTitle: "Repositories",
singularTitle: "Repository"
}
};
export const SECRET_SCANNING_FINDING_STATUS_ICON_MAP = {
[SecretScanningFindingStatus.Resolved]: { icon: faCheck, className: "text-green" },
[SecretScanningFindingStatus.Unresolved]: { icon: faWarning, className: "text-yellow" },
[SecretScanningFindingStatus.Ignore]: { icon: faBan, className: "text-mineshaft-400" },
[SecretScanningFindingStatus.FalsePositive]: {
icon: faMagnifyingGlassMinus,
className: "text-mineshaft-400"
}
};
@@ -1,6 +1,7 @@
export enum AppConnection {
AWS = "aws",
GitHub = "github",
GitHubRadar = "github-radar",
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,40 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import {
TGitHubRadarConnectionListRepositoriesResponse,
TGitHubRadarConnectionRepository
} from "./types";
const githubRadarConnectionKeys = {
all: [...appConnectionKeys.all, "github-radar"] as const,
listRepositories: (connectionId: string) =>
[...githubRadarConnectionKeys.all, "repositories", connectionId] as const
};
export const useGitHubRadarConnectionListRepositories = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TGitHubRadarConnectionRepository[],
unknown,
TGitHubRadarConnectionRepository[],
ReturnType<typeof githubRadarConnectionKeys.listRepositories>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: githubRadarConnectionKeys.listRepositories(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGitHubRadarConnectionListRepositoriesResponse>(
`/api/v1/app-connections/github-radar/${connectionId}/repositories`
);
return data.repositories;
},
...options
});
};
@@ -0,0 +1,14 @@
export type TGitHubRadarConnectionOrganization = {
login: string;
id: number;
};
export type TGitHubRadarConnectionRepository = {
id: number;
name: string;
owner: TGitHubRadarConnectionOrganization;
};
export type TGitHubRadarConnectionListRepositoriesResponse = {
repositories: TGitHubRadarConnectionRepository[];
};
@@ -17,6 +17,11 @@ export type TGitHubConnectionOption = TAppConnectionOptionBase & {
appClientSlug?: string;
};
export type TGitHubRadarConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.GitHubRadar;
appClientSlug?: string;
};
export type TGcpConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.GCP;
};
@@ -121,6 +126,7 @@ export type TAppConnectionOption =
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
[AppConnection.GitHub]: TGitHubConnectionOption;
[AppConnection.GitHubRadar]: TGitHubRadarConnectionOption;
[AppConnection.GCP]: TGcpConnectionOption;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum GitHubRadarConnectionMethod {
App = "github-app"
}
export type TGitHubRadarConnection = TRootAppConnection & { app: AppConnection.GitHubRadar } & {
method: GitHubRadarConnectionMethod.App;
credentials: {
code: string;
installationId: string;
};
};
@@ -10,6 +10,7 @@ import { TCamundaConnection } from "./camunda-connection";
import { TDatabricksConnection } from "./databricks-connection";
import { TGcpConnection } from "./gcp-connection";
import { TGitHubConnection } from "./github-connection";
import { TGitHubRadarConnection } from "./github-radar-connection";
import { THCVaultConnection } from "./hc-vault-connection";
import { THumanitecConnection } from "./humanitec-connection";
import { TLdapConnection } from "./ldap-connection";
@@ -32,6 +33,7 @@ export * from "./camunda-connection";
export * from "./databricks-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export * from "./github-radar-connection";
export * from "./hc-vault-connection";
export * from "./humanitec-connection";
export * from "./ldap-connection";
@@ -47,6 +49,7 @@ export * from "./windmill-connection";
export type TAppConnection =
| TAwsConnection
| TGitHubConnection
| TGitHubRadarConnection
| TGcpConnection
| TAzureKeyVaultConnection
| TAzureAppConfigurationConnection
@@ -95,6 +98,7 @@ export type TDeleteAppConnectionDTO = {
export type TAppConnectionMap = {
[AppConnection.AWS]: TAwsConnection;
[AppConnection.GitHub]: TGitHubConnection;
[AppConnection.GitHubRadar]: TGitHubRadarConnection;
[AppConnection.GCP]: TGcpConnection;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
@@ -191,6 +191,20 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
[EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity",
[EventType.SECRET_SCANNING_DATA_SOURCE_LIST]: "List Secret Scanning Data Sources",
[EventType.SECRET_SCANNING_DATA_SOURCE_CREATE]: "Create Secret Scanning Data Source",
[EventType.SECRET_SCANNING_DATA_SOURCE_UPDATE]: "Update Secret Scanning Data Source",
[EventType.SECRET_SCANNING_DATA_SOURCE_DELETE]: "Delete Secret Scanning Data Source",
[EventType.SECRET_SCANNING_DATA_SOURCE_GET]: "Get Secret Scanning Data Source",
[EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN]: "Trigger Secret Scanning Data Source Scan",
[EventType.SECRET_SCANNING_DATA_SOURCE_SCAN]: "Secret Scanning Data Source Scan",
[EventType.SECRET_SCANNING_RESOURCE_LIST]: "List Secret Scanning Resources",
[EventType.SECRET_SCANNING_SCAN_LIST]: "List Secret Scanning Scans",
[EventType.SECRET_SCANNING_FINDING_LIST]: "List Secret Scanning Findings",
[EventType.SECRET_SCANNING_FINDING_UPDATE]: "Update Secret Scanning Finding Status",
[EventType.SECRET_SCANNING_CONFIG_GET]: "Get Secret Scanning Configuration",
[EventType.SECRET_SCANNING_CONFIG_UPDATE]: "Update Secret Scanning Configuration",
[EventType.CREATE_PKI_SUBSCRIBER]: "Create PKI subscriber",
[EventType.UPDATE_PKI_SUBSCRIBER]: "Update PKI subscriber",
[EventType.DELETE_PKI_SUBSCRIBER]: "Delete PKI subscriber",
@@ -185,6 +185,20 @@ export enum EventType {
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
SECRET_SCANNING_DATA_SOURCE_GET = "secret-scanning-data-source-get",
SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN = "secret-scanning-data-source-trigger-scan",
SECRET_SCANNING_DATA_SOURCE_SCAN = "secret-scanning-data-source-scan",
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update",
SECRET_SCANNING_CONFIG_GET = "secret-scanning-config-get",
SECRET_SCANNING_CONFIG_UPDATE = "secret-scanning-config-update",
CREATE_PKI_SUBSCRIBER = "create-pki-subscriber",
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
+14 -1
View File
@@ -33,7 +33,8 @@ export enum DynamicSecretProviders {
Snowflake = "snowflake",
Totp = "totp",
SapAse = "sap-ase",
Kubernetes = "kubernetes"
Kubernetes = "kubernetes",
Vertica = "vertica"
}
export enum SqlProviders {
@@ -277,6 +278,18 @@ export type TDynamicSecretProvider =
sslEnabled: boolean;
audiences: string[];
};
}
| {
type: DynamicSecretProviders.Vertica;
inputs: {
host: string;
port: number;
database: string;
username: string;
password: string;
creationStatement: string;
revocationStatement: string;
};
};
export type TCreateDynamicSecretDTO = {
+35 -29
View File
@@ -1,3 +1,4 @@
import { useState } from "react";
import { MutationCache, QueryClient } from "@tanstack/react-query";
import axios from "axios";
@@ -18,6 +19,39 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
function ValidationErrorModal({ serverResponse }: { serverResponse: TApiErrors }) {
const [open, setOpen] = useState(true);
if (serverResponse.error !== ApiErrorTypes.ValidationError) {
return null;
}
return (
<Modal isOpen={open} onOpenChange={setOpen}>
<ModalContent title="Validation Error Details">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Field</Th>
<Th>Issue</Th>
</Tr>
</THead>
<TBody>
{serverResponse.message?.map(({ message, path }) => (
<Tr key={path.join(".")}>
<Td>{path.join(".")}</Td>
<Td>{message.toLowerCase()}</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
);
}
export const onRequestError = (error: unknown) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as TApiErrors;
@@ -27,35 +61,7 @@ export const onRequestError = (error: unknown) => {
title: "Validation Error",
type: "error",
text: "Please check the input and try again.",
callToAction: (
<Modal>
<ModalTrigger asChild>
<Button variant="outline_bg" size="xs">
Show more
</Button>
</ModalTrigger>
<ModalContent title="Validation Error Details">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Field</Th>
<Th>Issue</Th>
</Tr>
</THead>
<TBody>
{serverResponse.message?.map(({ message, path }) => (
<Tr key={path.join(".")}>
<Td>{path.join(".")}</Td>
<Td>{message.toLowerCase()}</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
),
callToAction: <ValidationErrorModal serverResponse={serverResponse} />,
copyActions: [
{
value: serverResponse.reqId,
@@ -0,0 +1,33 @@
export enum SecretScanningDataSource {
GitHub = "github"
}
export enum SecretScanningScanStatus {
Completed = "completed",
Failed = "failed",
Queued = "queued",
Scanning = "scanning"
}
export enum SecretScanningScanType {
FullScan = "full-scan",
DiffScan = "diff-scan"
}
export enum SecretScanningFindingStatus {
Resolved = "resolved",
Unresolved = "unresolved",
FalsePositive = "false-positive",
Ignore = "ignore"
}
export enum SecretScanningResource {
Repository = "repository",
Project = "project"
}
export enum SecretScanningFindingSeverity {
High = "high",
Medium = "medium",
Low = "low"
}
@@ -0,0 +1,4 @@
export * from "./enums";
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -0,0 +1,160 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { secretScanningV2Keys } from "./queries";
import {
TCreateSecretScanningDataSourceDTO,
TDeleteSecretScanningDataSourceDTO,
TGetSecretScanningConfigResponse,
TSecretScanningConfig,
TSecretScanningDataSourceResponse,
TSecretScanningFindingResponse,
TTriggerSecretScanningDataSourceDTO,
TUpdateSecretScanningDataSourceDTO,
TUpdateSecretScanningFinding
} from "./types";
export const useCreateSecretScanningDataSource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ type, ...params }: TCreateSecretScanningDataSourceDTO) => {
const { data } = await apiRequest.post<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}`,
params
);
return data.dataSource;
},
onSuccess: (_, { projectId }) =>
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listDataSources(projectId)
})
});
};
export const useUpdateSecretScanningDataSource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ type, dataSourceId, ...params }: TUpdateSecretScanningDataSourceDTO) => {
const { data } = await apiRequest.patch<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`,
params
);
return data.dataSource;
},
onSuccess: (_, { projectId, dataSourceId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listDataSources(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listResources(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listScans(dataSourceId)
});
}
});
};
export const useDeleteSecretScanningDataSource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ type, dataSourceId }: TDeleteSecretScanningDataSourceDTO) => {
const { data } = await apiRequest.delete<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`
);
return data.dataSource;
},
onSuccess: (_, { projectId, dataSourceId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listDataSources(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listResources(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listScans(dataSourceId)
});
}
});
};
export const useTriggerSecretScanningDataSource = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ type, dataSourceId, resourceId }: TTriggerSecretScanningDataSourceDTO) => {
const { data } = await apiRequest.post<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}${resourceId ? `/resources/${resourceId}` : ""}/scan`
);
return data.dataSource;
},
onSuccess: (_, { projectId, dataSourceId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listDataSources(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listResources(dataSourceId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listScans(dataSourceId)
});
}
});
};
export const useUpdateSecretScanningFinding = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ findingId, ...params }: TUpdateSecretScanningFinding) => {
const { data } = await apiRequest.patch<TSecretScanningFindingResponse>(
`/api/v2/secret-scanning/findings/${findingId}`,
params
);
return data.finding;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.listFindings(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.findingCount(projectId)
});
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.dataSource()
});
}
});
};
export const useUpdateSecretScanningConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ projectId, ...params }: TSecretScanningConfig) => {
const { data } = await apiRequest.patch<TGetSecretScanningConfigResponse>(
`/api/v2/secret-scanning/configs?projectId=${projectId}`,
params
);
return data.config;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: secretScanningV2Keys.configByProjectId(projectId)
});
}
});
};
@@ -0,0 +1,248 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
TGetSecretScanningConfigResponse,
TGetSecretScanningDataSource,
TGetSecretScanningUnresolvedFindingsResponse,
TListSecretScanningDataSourceOptions,
TListSecretScanningDataSources,
TListSecretScanningFindingsResponse,
TListSecretScanningResourcesResponse,
TListSecretScanningScansResponse,
TSecretScanningConfig,
TSecretScanningDataSource,
TSecretScanningDataSourceOption,
TSecretScanningDataSourceResponse,
TSecretScanningDataSourceWithDetails,
TSecretScanningFinding,
TSecretScanningResourceWithDetails,
TSecretScanningScanWithDetails
} from "./types";
export const secretScanningV2Keys = {
all: ["secret-scanning-v2"] as const,
dataSource: () => [...secretScanningV2Keys.all, "data-source"] as const,
dataSourceOptions: () => [...secretScanningV2Keys.dataSource(), "options"] as const,
dataSourceById: (dataSourceId: string) => [
...secretScanningV2Keys.dataSource(),
"byId",
dataSourceId
],
listDataSources: (projectId: string) => [...secretScanningV2Keys.dataSource(), "list", projectId],
listResources: (dataSourceId: string) => [
...secretScanningV2Keys.dataSource(),
"resources",
dataSourceId
],
listScans: (dataSourceId: string) => [
...secretScanningV2Keys.dataSource(),
"scans",
dataSourceId
],
finding: () => [...secretScanningV2Keys.all, "finding"] as const,
findingCount: (projectId: string) =>
[...secretScanningV2Keys.finding(), "count", projectId] as const,
listFindings: (projectId: string) => [...secretScanningV2Keys.finding(), "list", projectId],
configByProjectId: (projectId: string) =>
[...secretScanningV2Keys.all, "config", projectId] as const
};
export const useSecretScanningDataSourceOptions = (
options?: Omit<
UseQueryOptions<
TSecretScanningDataSourceOption[],
unknown,
TSecretScanningDataSourceOption[],
ReturnType<typeof secretScanningV2Keys.dataSourceOptions>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.dataSourceOptions(),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningDataSourceOptions>(
"/api/v2/secret-scanning/data-sources/options"
);
return data.dataSourceOptions;
},
...options
});
};
export const useListSecretScanningDataSources = (
projectId: string,
options?: Omit<
UseQueryOptions<
TSecretScanningDataSourceWithDetails[],
unknown,
TSecretScanningDataSourceWithDetails[],
ReturnType<typeof secretScanningV2Keys.listDataSources>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.listDataSources(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningDataSources>(
"/api/v2/secret-scanning/data-sources-dashboard",
{ params: { projectId } }
);
return data.dataSources;
},
...options
});
};
export const useGetSecretScanningDataSource = (
{ dataSourceId, type }: TGetSecretScanningDataSource,
options?: Omit<
UseQueryOptions<
TSecretScanningDataSource,
unknown,
TSecretScanningDataSource,
ReturnType<typeof secretScanningV2Keys.dataSourceById>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId),
queryFn: async () => {
const { data } = await apiRequest.get<TSecretScanningDataSourceResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`
);
return data.dataSource;
},
...options
});
};
export const useListSecretScanningResources = (
{ dataSourceId, type }: TGetSecretScanningDataSource,
options?: Omit<
UseQueryOptions<
TSecretScanningResourceWithDetails[],
unknown,
TSecretScanningResourceWithDetails[],
ReturnType<typeof secretScanningV2Keys.listResources>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.listResources(dataSourceId),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningResourcesResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}/resources-dashboard`
);
return data.resources;
},
...options
});
};
export const useListSecretScanningScans = (
{ dataSourceId, type }: TGetSecretScanningDataSource,
options?: Omit<
UseQueryOptions<
TSecretScanningScanWithDetails[],
unknown,
TSecretScanningScanWithDetails[],
ReturnType<typeof secretScanningV2Keys.listScans>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.listScans(dataSourceId),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningScansResponse>(
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}/scans-dashboard`
);
return data.scans;
},
...options
});
};
export const useGetSecretScanningUnresolvedFindingCount = (
projectId: string,
options?: Omit<
UseQueryOptions<number, unknown, number, ReturnType<typeof secretScanningV2Keys.findingCount>>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.findingCount(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TGetSecretScanningUnresolvedFindingsResponse>(
"/api/v2/secret-scanning/unresolved-findings-count",
{ params: { projectId } }
);
return data.unresolvedFindings;
},
...options
});
};
export const useListSecretScanningFindings = (
projectId: string,
options?: Omit<
UseQueryOptions<
TSecretScanningFinding[],
unknown,
TSecretScanningFinding[],
ReturnType<typeof secretScanningV2Keys.listFindings>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.listFindings(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretScanningFindingsResponse>(
"/api/v2/secret-scanning/findings",
{ params: { projectId } }
);
return data.findings;
},
...options
});
};
export const useGetSecretScanningConfig = (
projectId: string,
options?: Omit<
UseQueryOptions<
TSecretScanningConfig,
unknown,
TSecretScanningConfig,
ReturnType<typeof secretScanningV2Keys.configByProjectId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretScanningV2Keys.configByProjectId(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TGetSecretScanningConfigResponse>(
"/api/v2/secret-scanning/configs",
{ params: { projectId } }
);
return data.config;
},
...options
});
};
@@ -0,0 +1,17 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretScanningDataSource } from "../enums";
import { TSecretScanningDataSourceBase } from "./shared";
export type TGitHubDataSource = TSecretScanningDataSourceBase & {
type: SecretScanningDataSource.GitHub;
config: {
includeRepos: string[];
};
};
export type TGitHubDataSourceOption = {
name: string;
type: SecretScanningDataSource.GitHub;
connection: AppConnection.GitHub;
};
@@ -0,0 +1,166 @@
import { DiscriminativePick } from "@app/types";
import {
SecretScanningDataSource,
SecretScanningFindingSeverity,
SecretScanningFindingStatus,
SecretScanningResource,
SecretScanningScanStatus,
SecretScanningScanType
} from "../enums";
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
export type TSecretScanningDataSource = TGitHubDataSource;
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
lastScannedAt: string | null;
lastScanStatus: SecretScanningScanStatus | null;
lastScanStatusMessage: string | null;
unresolvedFindings: number | null;
};
export type TListSecretScanningDataSources = {
dataSources: TSecretScanningDataSourceWithDetails[];
};
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption;
export type TListSecretScanningDataSourceOptions = {
dataSourceOptions: TSecretScanningDataSourceOption[];
};
export type TSecretScanningDataSourceResponse = { dataSource: TSecretScanningDataSource };
export type TCreateSecretScanningDataSourceDTO = DiscriminativePick<
TSecretScanningDataSource,
"name" | "config" | "description" | "connectionId" | "type" | "isAutoScanEnabled" | "projectId"
>;
export type TUpdateSecretScanningDataSourceDTO = Partial<
Omit<TCreateSecretScanningDataSourceDTO, "type" | "connectionId" | "projectId">
> & {
type: SecretScanningDataSource;
dataSourceId: string;
// required for query invalidation
projectId: string;
};
export type TDeleteSecretScanningDataSourceDTO = {
type: SecretScanningDataSource;
dataSourceId: string;
// required for query invalidation
projectId: string;
};
export type TTriggerSecretScanningDataSourceDTO = {
type: SecretScanningDataSource;
dataSourceId: string;
resourceId?: string;
// required for query invalidation
projectId: string;
};
export type TUpdateSecretScanningFinding = {
findingId: string;
status: SecretScanningFindingStatus;
remarks?: string | null;
// required for query invalidation
projectId: string;
};
export type TSecretScanningFindingResponse = {
finding: TSecretScanningFinding;
};
export type TGetSecretScanningDataSource = {
dataSourceId: string;
type: SecretScanningDataSource;
};
export type TSecretScanningResourceWithDetails = {
id: string;
dataSourceId: string;
type: SecretScanningResource;
externalId: string;
name: string;
createdAt: string;
updatedAt: string;
lastScannedAt: string;
lastScanStatus: SecretScanningScanStatus;
lastScanStatusMessage: string | null;
unresolvedFindings: number;
};
export type TListSecretScanningResourcesResponse = {
resources: TSecretScanningResourceWithDetails[];
};
export type TSecretScanningScanWithDetails = {
id: string;
createdAt: string;
resourceId: string;
type: SecretScanningScanType;
status: SecretScanningScanStatus;
statusMessage?: string | null;
unresolvedFindings: number;
resolvedFindings: number;
resourceName: string;
};
export type TListSecretScanningScansResponse = {
scans: TSecretScanningScanWithDetails[];
};
export type TGetSecretScanningUnresolvedFindingsResponse = {
unresolvedFindings: number;
};
export type TSecretScanningConfig = {
projectId: string;
content: string | null;
};
export type TGetSecretScanningConfigResponse = {
config: TSecretScanningConfig;
};
export type TSecretScanningFinding = {
id: string;
dataSourceName: string;
dataSourceType: SecretScanningDataSource;
resourceName: string;
resourceType: SecretScanningResource;
rule: string;
severity: SecretScanningFindingSeverity;
status: SecretScanningFindingStatus;
remarks?: string;
fingerprint: string;
// TODO scott: this will need to be type differentiated once we add other scan types
details: {
description: string;
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
file: string;
link: string;
symlinkFile: string;
commit: string;
entropy: string;
author: string;
email: string;
date: string;
message: string;
tags: string[];
ruleID: string;
fingerprint: string;
};
projectId: string;
scanId: string;
createdAt: string;
updatedAt: string;
};
export type TListSecretScanningFindingsResponse = {
findings: TSecretScanningFinding[];
};
@@ -0,0 +1 @@
export * from "./secret-scanning-data-source-base";
@@ -0,0 +1,18 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
export type TSecretScanningDataSourceBase = {
id: string;
name: string;
description?: string | null;
connectionId?: string | null;
createdAt: string;
updatedAt: string;
projectId: string;
connection?: {
app: AppConnection;
id: string;
name: string;
} | null;
isAutoScanEnabled: boolean;
isDisconnected: boolean;
};
@@ -50,6 +50,7 @@ export type SubscriptionPlan = {
enforceMfa: boolean;
projectTemplates: boolean;
kmip: boolean;
secretScanning: boolean;
enterpriseSecretSyncs: boolean;
enterpriseAppConnections: boolean;
};
+2 -1
View File
@@ -12,7 +12,8 @@ export enum ProjectType {
SecretManager = "secret-manager",
CertificateManager = "cert-manager",
KMS = "kms",
SSH = "ssh"
SSH = "ssh",
SecretScanning = "secret-scanning"
}
export enum ProjectUserMembershipTemporaryMode {
+4 -2
View File
@@ -6,16 +6,18 @@ import { useDebounce } from "@app/hooks/useDebounce";
export const usePagination = <T extends string>(
initialOrderBy: T,
{
initPerPage = 100
initPerPage = 100,
initSearch = ""
}: {
initPerPage?: number;
initSearch?: string;
} = {}
) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(initPerPage);
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
const [search, setSearch] = useState("");
const [search, setSearch] = useState(initSearch);
const [debouncedSearch] = useDebounce(search);
const offset = (page - 1) * perPage;
@@ -55,16 +55,15 @@ export const OrganizationLayout = () => {
linkOptions({ to: "/organization/kms/overview" }).to,
linkOptions({ to: "/organization/kms/settings" }).to,
linkOptions({ to: "/organization/ssh/overview" }).to,
linkOptions({ to: "/organization/ssh/settings" }).to
linkOptions({ to: "/organization/ssh/settings" }).to,
linkOptions({ to: "/organization/secret-scanning/overview" }).to,
linkOptions({ to: "/organization/secret-scanning/settings" }).to
] as string[]
).includes(location.pathname);
const shouldShowOrgSidebar =
location.pathname.startsWith("/organization") &&
(!isSecretSharingPage || shouldShowProductsSidebar) &&
!([linkOptions({ to: "/organization/secret-scanning" }).to] as string[]).includes(
location.pathname
);
(!isSecretSharingPage || shouldShowProductsSidebar);
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
@@ -42,6 +42,11 @@ export const ProjectOverviewSideBar = ({ type }: TProjectOverviewSideBarProps) =
icon = "verified";
link = "https://infisical.com/docs/documentation/platform/ssh/overview";
break;
case ProjectType.SecretScanning:
label = "Secret Scanning";
icon = "secret-scan";
link = "https://infisical.com/docs/documentation/platform/secret-scanning/overview";
break;
default:
throw new Error("Unknown project type");
}
@@ -332,18 +332,26 @@ export const MinimizedOrgSidebar = () => {
)}
</Link>
)}
{(currentOrg.scannerProductEnabled || currentOrg.shareSecretsProductEnabled) && (
<div className="w-full bg-mineshaft-500" style={{ height: "1px" }} />
)}
{currentOrg.scannerProductEnabled && (
<Link to="/organization/secret-scanning">
<Link to="/organization/secret-scanning/overview">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan">
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(
`/organization/${ProjectType.SecretScanning}`
)
}
icon="secret-scan"
>
Scanner
</MenuIconButton>
)}
</Link>
)}
{(currentOrg.scannerProductEnabled || currentOrg.shareSecretsProductEnabled) && (
<div className="w-full bg-mineshaft-500" style={{ height: "1px" }} />
)}
{currentOrg.shareSecretsProductEnabled && (
<Link to="/organization/secret-sharing">
{({ isActive }) => (
@@ -6,6 +6,7 @@ import { motion } from "framer-motion";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
BreadcrumbContainer,
Menu,
MenuGroup,
@@ -19,11 +20,13 @@ import {
useSubscription,
useWorkspace
} from "@app/context";
import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types";
import {
useGetAccessRequestsCount,
useGetSecretApprovalRequestCount,
useGetSecretRotations
} from "@app/hooks/api";
import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secretScanningV2";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AssumePrivilegeModeBanner } from "./components/AssumePrivilegeModeBanner";
@@ -36,6 +39,8 @@ export const ProjectLayout = () => {
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const { permission } = useProjectPermission();
const { t } = useTranslation();
const { assumedPrivilegeDetails } = useProjectPermission();
const workspaceId = currentWorkspace?.id || "";
@@ -46,6 +51,7 @@ export const ProjectLayout = () => {
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
const isCmek = currentWorkspace?.type === ProjectType.KMS;
const isSSH = currentWorkspace?.type === ProjectType.SSH;
const isSecretScanning = currentWorkspace?.type === ProjectType.SecretScanning;
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
workspaceId,
@@ -68,6 +74,17 @@ export const ProjectLayout = () => {
const pendingRequestsCount =
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(workspaceId, {
enabled:
isSecretScanning &&
subscription.secretScanning &&
permission.can(
ProjectPermissionSecretScanningFindingActions.Read,
ProjectPermissionSub.SecretScanningFindings
),
refetchInterval: 30000
});
return (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
@@ -258,6 +275,41 @@ export const ProjectLayout = () => {
</ProjectPermissionCan>
</>
)}
{isSecretScanning && (
<Link
to={`/${ProjectType.SecretScanning}/$projectId/data-sources` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="blocks">
Data Sources
</MenuItem>
)}
</Link>
)}
{isSecretScanning && (
<Link
to={`/${ProjectType.SecretScanning}/$projectId/findings` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="search">
<div className="flex w-full items-center justify-between">
<span>Findings</span>
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="mr-2">
{unresolvedFindings}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
)}
{isSecretManager && (
<Link
to={`/${ProjectType.SecretManager}/$projectId/integrations` as const}
@@ -5,6 +5,7 @@ import { Button } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const AssumePrivilegeModeBanner = () => {
const { currentWorkspace } = useWorkspace();
@@ -35,7 +36,20 @@ export const AssumePrivilegeModeBanner = () => {
},
{
onSuccess: () => {
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/overview`;
let page: string;
switch (currentWorkspace.type) {
case ProjectType.SecretScanning:
page = "data-sources";
break;
case ProjectType.CertificateManager:
page = "subscribers";
break;
default:
page = "overview";
}
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/${page}`;
}
}
);
+5
View File
@@ -8,6 +8,11 @@ export const formatReservedPaths = (secretPath: string) => {
return secretPath;
};
export const parsePathFromReplicatedPath = (secretPath: string) => {
const i = secretPath.indexOf(ReservedFolders.SecretReplication);
return secretPath.slice(0, i);
};
export const camelCaseToSpaces = (input: string) => {
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
};
@@ -164,7 +164,7 @@ export const MachineIdentitiesTable = () => {
<IdentityPanelTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure want to remove Server Admin permissions from ${
title={`Are you sure you want to remove Server Admin permissions from ${
(popUp?.removeServerAdmin?.data as { name: string })?.name || ""
}?`}
subTitle=""
@@ -309,7 +309,7 @@ export const UserIdentitiesTable = () => {
/>
<DeleteActionModal
isOpen={popUp.upgradeToServerAdmin.isOpen}
title={`Are you sure want to grant Server Admin permissions to ${
title={`Are you sure you want to grant Server Admin permissions to ${
(popUp?.upgradeToServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
@@ -320,7 +320,7 @@ export const UserIdentitiesTable = () => {
/>
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure want to remove Server Admin permissions from ${
title={`Are you sure you want to remove Server Admin permissions from ${
(popUp?.removeServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
@@ -70,7 +70,7 @@ export const PkiAlertsSection = () => {
<PkiAlertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deletePkiAlert.isOpen}
title={`Are you sure want to remove the alert ${
title={`Are you sure you want to remove the alert ${
(popUp?.deletePkiAlert?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiAlert", isOpen)}
@@ -70,7 +70,7 @@ export const PkiCollectionSection = () => {
<PkiCollectionModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deletePkiCollection.isOpen}
title={`Are you sure want to remove the alert ${
title={`Are you sure you want to remove the alert ${
(popUp?.deletePkiCollection?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiCollection", isOpen)}
@@ -135,7 +135,7 @@ const Page = () => {
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCa.isOpen}
title={`Are you sure want to remove the CA ${
title={`Are you sure you want to remove the CA ${
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
} from the project?`}
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
@@ -96,7 +96,7 @@ export const CaSection = () => {
<CaTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.deleteCa.isOpen}
title={`Are you sure want to remove the CA ${
title={`Are you sure you want to remove the CA ${
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
} from the project?`}
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
@@ -108,7 +108,7 @@ export const CaSection = () => {
/>
<DeleteActionModal
isOpen={popUp.caStatus.isOpen}
title={`Are you sure want to ${
title={`Are you sure you want to ${
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
? "enable"
: "disable"
@@ -97,7 +97,7 @@ export const ExternalCaSection = () => {
<ExternalCaTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.deleteCa.isOpen}
title={`Are you sure want to remove the CA ${
title={`Are you sure you want to remove the CA ${
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
} from the project?`}
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
@@ -112,7 +112,7 @@ export const ExternalCaSection = () => {
/>
<DeleteActionModal
isOpen={popUp.caStatus.isOpen}
title={`Are you sure want to ${
title={`Are you sure you want to ${
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
? "enable"
: "disable"
@@ -90,7 +90,7 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCertificateTemplate.isOpen}
title={`Are you sure want to delete the certificate template ${
title={`Are you sure you want to delete the certificate template ${
(popUp?.deleteCertificateTemplate?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteCertificateTemplate", isOpen)}
@@ -89,7 +89,7 @@ export const CertificatesSection = () => {
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCertificate.isOpen}
title={`Are you sure want to remove the certificate ${
title={`Are you sure you want to remove the certificate ${
(popUp?.deleteCertificate?.data as { commonName: string })?.commonName || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteCertificate", isOpen)}
@@ -152,7 +152,7 @@ export const PkiCollectionPage = () => {
<PkiCollectionModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deletePkiCollection.isOpen}
title={`Are you sure want to delete the PKI collection ${data?.name ?? ""}?`}
title={`Are you sure you want to delete the PKI collection ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiCollection", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
@@ -74,7 +74,7 @@ export const PkiCollectionItemsSection = ({ collectionId, type }: Props) => {
/>
<DeleteActionModal
isOpen={popUp.deletePkiCollectionItem.isOpen}
title={`Are you sure want to remove the item from the collection ${pkiCollection.name}?`}
title={`Are you sure you want to remove the item from the collection ${pkiCollection.name}?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiCollectionItem", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
@@ -1,5 +1,4 @@
import { Controller, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -79,14 +79,6 @@ export const DeleteProjectSection = () => {
};
const handleLeaveWorkspaceSubmit = async () => {
console.log({
currentWorkspace,
currentOrg,
members,
isNoAccessMember,
membership
});
try {
setIsLeaving.on();
@@ -170,7 +162,7 @@ export const DeleteProjectSection = () => {
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
title="Are you sure you want to delete this project?"
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
@@ -180,8 +172,8 @@ export const DeleteProjectSection = () => {
<LeaveProjectModal
isOpen={popUp.leaveWorkspace.isOpen}
title="Are you sure want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and it's contents.`}
title="Are you sure you want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and its contents.`}
onChange={(isOpen) => handlePopUpToggle("leaveWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Leave Project"
@@ -44,7 +44,7 @@ export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Prop
return (
<DeleteActionModal
isOpen={isOpen}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
onChange={onOpenChange}
deleteKey="confirm"
onDeleteApproved={handleDeleteKmipClient}
@@ -43,7 +43,7 @@ export const DeleteCmekModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<DeleteActionModal
isOpen={isOpen}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
onChange={onOpenChange}
deleteKey="confirm"
onDeleteApproved={handleDeleteCmek}
@@ -79,14 +79,6 @@ export const DeleteProjectSection = () => {
};
const handleLeaveWorkspaceSubmit = async () => {
console.log({
currentWorkspace,
currentOrg,
members,
isNoAccessMember,
membership
});
try {
setIsLeaving.on();
@@ -170,7 +162,7 @@ export const DeleteProjectSection = () => {
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
title="Are you sure you want to delete this project?"
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
@@ -180,8 +172,8 @@ export const DeleteProjectSection = () => {
<LeaveProjectModal
isOpen={popUp.leaveWorkspace.isOpen}
title="Are you sure want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and it's contents.`}
title="Are you sure you want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and its contents.`}
onChange={(isOpen) => handlePopUpToggle("leaveWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Leave Project"
@@ -80,7 +80,7 @@ export const OrgGroupsSection = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
title={`Are you sure you want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
@@ -127,7 +127,7 @@ export const IdentitySection = withPermission(
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
@@ -143,7 +143,7 @@ export const OrgMembersSection = () => {
/>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with username ${
title={`Are you sure you want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
@@ -156,7 +156,7 @@ export const OrgMembersSection = () => {
/>
<DeleteActionModal
isOpen={popUp.deactivateMember.isOpen}
title={`Are you sure want to deactivate member with username ${
title={`Are you sure you want to deactivate member with username ${
(popUp?.deactivateMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
@@ -1,8 +1,11 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faCheckCircle,
faChevronRight,
faEllipsis,
faFilter,
faMagnifyingGlass,
faSearch,
faUsers
@@ -19,7 +22,11 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger,
EmptyState,
IconButton,
Input,
@@ -75,6 +82,10 @@ enum OrgMembersOrderBy {
Email = "email"
}
type Filter = {
roles: string[];
};
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => {
const navigate = useNavigate();
const { subscription } = useSubscription();
@@ -184,17 +195,29 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
setUserTablePreference("orgMembersTable", PreferenceKey.PerPage, newPerPage);
};
const [filter, setFilter] = useState<Filter>({
roles: []
});
const filteredUsers = useMemo(
() =>
members
?.filter(
({ user: u, inviteEmail }) =>
?.filter(({ user: u, inviteEmail, role, roleId }) => {
if (
filter.roles.length &&
!filter.roles.includes(role === "custom" ? findRoleFromId(roleId)!.slug : role)
) {
return false;
}
return (
u?.firstName?.toLowerCase().includes(search.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(search.toLowerCase()) ||
u?.username?.toLowerCase().includes(search.toLowerCase()) ||
u?.email?.toLowerCase().includes(search.toLowerCase()) ||
inviteEmail?.toLowerCase().includes(search.toLowerCase())
)
);
})
.sort((a, b) => {
const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
@@ -217,7 +240,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase());
}),
[members, search, orderDirection, orderBy]
[members, search, orderDirection, orderBy, filter]
);
const handleSort = (column: OrgMembersOrderBy) => {
@@ -236,14 +259,81 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
setPage
});
const handleRoleToggle = useCallback(
(roleSlug: string) =>
setFilter((state) => {
const currentRoles = state.roles || [];
if (currentRoles.includes(roleSlug)) {
return { ...state, roles: currentRoles.filter((role) => role !== roleSlug) };
}
return { ...state, roles: [...currentRoles, roleSlug] };
}),
[]
);
const isTableFiltered = Boolean(filter.roles.length);
return (
<div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter Users"
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 align="start" className="p-0">
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
<DropdownSubMenu>
<DropdownSubMenuTrigger
iconPos="right"
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
>
Roles
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Roles to Filter Users
</DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleRoleToggle(slug);
}}
key={id}
icon={filter.roles.includes(slug) && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: "#bec2c8" }}
/>
{name}
</div>
</DropdownMenuItem>
))}
</DropdownSubMenuContent>
</DropdownSubMenu>
</DropdownMenuContent>
</DropdownMenu>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
@@ -280,7 +280,7 @@ export const OrgRoleTable = () => {
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteRole?.data as TOrgRole)?.name || " "
} role?`}
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
@@ -67,6 +67,15 @@ export const OrgAdminProjects = withPermission(
});
return;
}
if (type === ProjectType.SecretScanning) {
await navigate({
to: "/secret-scanning/$projectId/data-sources" as const,
params: {
projectId
}
});
return;
}
await navigate({
to: `/${type}/$projectId/overview` as const,
params: {
@@ -19,6 +19,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { GitHubRadarConnectionForm } from "./GitHubRadarConnectionForm";
import { HCVaultConnectionForm } from "./HCVaultConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { LdapConnectionForm } from "./LdapConnectionForm";
@@ -72,6 +73,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
@@ -150,6 +153,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm appConnection={appConnection} />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm appConnection={appConnection} />;
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
@@ -188,7 +193,6 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <OCIConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.OnePass:
return <OnePassConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -0,0 +1,157 @@
import crypto from "crypto";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
GitHubRadarConnectionMethod,
TGitHubRadarConnection,
useGetAppConnectionOption
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TGitHubRadarConnection;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.GitHubRadar),
method: z.nativeEnum(GitHubRadarConnectionMethod)
});
type FormData = z.infer<typeof formSchema>;
export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
const {
option: { appClientSlug },
isLoading
} = useGetAppConnectionOption(AppConnection.GitHubRadar);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.GitHubRadar,
method: GitHubRadarConnectionMethod.App
}
});
const {
handleSubmit,
control,
watch,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubRadarConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case GitHubRadarConnectionMethod.App:
window.location.assign(
`https://github.com/apps/${appClientSlug}/installations/new?state=${state}`
);
break;
default:
throw new Error(
`Unhandled GitHub Radar Connection method: ${(formData as FormData).method}`
);
}
};
let isMissingConfig: boolean;
switch (selectedMethod) {
case GitHubRadarConnectionMethod.App:
isMissingConfig = !appClientSlug;
break;
default:
throw new Error(`Unhandled GitHub Radar Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.GitHubRadar].name
}. This field cannot be changed after creation.`}
errorText={
!isLoading && isMissingConfig
? `Environment variables have not been configured. ${
isInfisicalCloud()
? "Please contact Infisical."
: `See Docs to configure GitHub Radar ${methodDetails.name} Connections.`
}`
: error?.message
}
isError={Boolean(error?.message) || isMissingConfig}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(GitHubRadarConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isRedirecting}
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
>
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -15,11 +15,19 @@ export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
return (
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
<img
alt={`${appDetails.name} logo`}
src={`/images/integrations/${appDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div className="relative">
<img
alt={`${appDetails.name} logo`}
src={`/images/integrations/${appDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
{appDetails.icon && (
<FontAwesomeIcon
icon={appDetails.icon}
className="absolute bottom-1 right-1 text-primary-700"
/>
)}
</div>
<div>
<div className="flex items-center text-mineshaft-300">
{appDetails.name}
@@ -31,7 +31,7 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
return (
<div className="grid grid-cols-4 gap-2">
{appConnectionOptions?.map((option) => {
const { image, name, size = 50, enterprise = false } = APP_CONNECTION_MAP[option.app];
const { image, name, size = 50, enterprise = false, icon } = APP_CONNECTION_MAP[option.app];
return (
<button
@@ -43,14 +43,23 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="relative">
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
{icon && (
<FontAwesomeIcon
className="absolute -bottom-1.5 -right-1.5 text-primary-700"
size="xl"
icon={icon}
/>
)}
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
@@ -65,6 +65,8 @@ export const AppConnectionRow = ({
const methodDetails = getAppConnectionMethodDetails(method);
const connectionDetails = APP_CONNECTION_MAP[app];
return (
<Tr
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700")}
@@ -72,12 +74,21 @@ export const AppConnectionRow = ({
>
<Td>
<div className="flex items-center gap-2">
<img
alt={`${APP_CONNECTION_MAP[app].name} integration`}
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
className="mr-0.5 w-5"
/>
<span className="hidden lg:inline">{APP_CONNECTION_MAP[app].name}</span>
<div className="relative">
<img
alt={`${connectionDetails.name} integration`}
src={`/images/integrations/${connectionDetails.image}`}
className="mr-0.5 w-5"
/>
{connectionDetails.icon && (
<FontAwesomeIcon
icon={connectionDetails.icon}
size="xs"
className="absolute -bottom-0.5 -right-0.5 text-primary-700"
/>
)}
</div>
<span className="hidden lg:inline">{connectionDetails.name}</span>
</div>
</Td>
<Td className="!min-w-[8rem] max-w-0">
@@ -44,7 +44,7 @@ export const DeleteAppConnectionModal = ({ isOpen, onOpenChange, appConnection }
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteAppConnection}
>
@@ -14,6 +14,7 @@ import {
TAzureClientSecretsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
@@ -26,6 +27,9 @@ type BaseFormData = {
type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" | "description">;
type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
@@ -40,6 +44,7 @@ type AzureClientSecretsFormData = BaseFormData &
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
@@ -51,6 +56,7 @@ type FormDataMap = {
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData"
@@ -318,6 +324,54 @@ export const OAuthCallbackPage = () => {
};
}, []);
const handleGithubRadar = useCallback(async () => {
const formData = getFormData(AppConnection.GitHubRadar);
if (formData === null) return null;
clearState(AppConnection.GitHubRadar);
const { connectionId, name, description, returnUrl } = formData;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
connectionId,
credentials: {
code: code as string,
installationId: installationId as string
}
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
name,
description,
method: GitHubConnectionMethod.App,
credentials: {
code: code as string,
installationId: installationId as string
}
});
}
} catch (e: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} GitHub Radar Connection`,
text: e.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
});
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
};
}, []);
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
useEffect(() => {
if (!isReady) {
@@ -334,6 +388,8 @@ export const OAuthCallbackPage = () => {
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
} else if (appConnection === AppConnection.GitHubRadar) {
data = await handleGithubRadar();
} else if (appConnection === AppConnection.AzureKeyVault) {
data = await handleAzureKeyVault();
} else if (appConnection === AppConnection.AzureAppConfiguration) {
@@ -229,7 +229,7 @@ export const GatewayListPage = withPermission(
)}
<DeleteActionModal
isOpen={popUp.deleteGateway.isOpen}
title={`Are you sure want to delete gateway ${
title={`Are you sure you want to delete gateway ${
(popUp?.deleteGateway?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
@@ -157,7 +157,7 @@ const Page = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
title={`Are you sure you want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
@@ -94,7 +94,7 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
<AddGroupMembersModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.removeMemberFromGroup.isOpen}
title={`Are you sure want to remove ${
title={`Are you sure you want to remove ${
(popUp?.removeMemberFromGroup?.data as { username: string })?.username || ""
} from the group?`}
onChange={(isOpen) => handlePopUpToggle("removeMemberFromGroup", isOpen)}
@@ -101,7 +101,7 @@ const Page = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
@@ -66,7 +66,7 @@ export const IdentityProjectsSection = ({ identityId }: Props) => {
</div>
<DeleteActionModal
isOpen={popUp.removeIdentityFromProject.isOpen}
title={`Are you sure want to remove ${
title={`Are you sure you want to remove ${
(popUp?.removeIdentityFromProject?.data as { identityName: string })?.identityName || ""
} from ${
(popUp?.removeIdentityFromProject?.data as { projectName: string })?.projectName || ""
@@ -218,7 +218,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeToken.isOpen}
title={`Are you sure want to revoke ${
title={`Are you sure you want to revoke ${
(popUp?.revokeToken?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
@@ -175,7 +175,7 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
title={`Are you sure you want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
""
}************?`}
@@ -151,7 +151,7 @@ export const Content = ({
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
title={`Are you sure you want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
@@ -155,7 +155,7 @@ export const Page = () => {
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteOrgRole.isOpen}
title={`Are you sure want to delete the organization role ${data?.name ?? ""}?`}
title={`Are you sure you want to delete the organization role ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrgRole", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => onDeleteOrgRoleSubmit()}
@@ -24,6 +24,8 @@ const formatDescription = (type: ProjectType) => {
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
if (type === ProjectType.KMS)
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
if (type === ProjectType.SecretScanning)
return "Connect and monitor data sources to prevent secret leaks.";
return "Infisical SSH lets you issue SSH credentials to users for short-lived, secure SSH access to infrastructure.";
};

Some files were not shown because too many files have changed in this diff Show More