mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 05:19:49 +00:00
feature: secret scanning architecture and github data source (wip)
This commit is contained in:
@@ -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 deleteSecretRotation = 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 deleteSecretRotation.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 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>
|
||||
);
|
||||
};
|
||||
+126
@@ -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]);
|
||||
|
||||
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't see the repository you'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?.find((project) => value.includes(project.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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+55
@@ -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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceConfigFields";
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
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
|
||||
);
|
||||
|
||||
const appName = APP_CONNECTION_MAP[app].name;
|
||||
|
||||
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 {appName} Connections. Create one from the{" "}
|
||||
<Link to="/organization/app-connections" className="underline">
|
||||
App Connections
|
||||
</Link>{" "}
|
||||
page.
|
||||
</>
|
||||
) : (
|
||||
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+51
@@ -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 rotation = await mutation;
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${dataSource ? "updated" : "created"} ${sourceType} Data Source`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(rotation);
|
||||
} 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>
|
||||
);
|
||||
};
|
||||
+27
@@ -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>
|
||||
);
|
||||
};
|
||||
+34
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceReviewFields";
|
||||
+16
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceConfigReviewSection";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceForm";
|
||||
+18
@@ -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,14 @@
|
||||
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")
|
||||
})
|
||||
})
|
||||
.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";
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -113,6 +113,21 @@ 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",
|
||||
Resolve = "resolve-findings"
|
||||
}
|
||||
|
||||
export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$ALL = "$all",
|
||||
@@ -199,7 +214,9 @@ export enum ProjectPermissionSub {
|
||||
Kms = "kms",
|
||||
Cmek = "cmek",
|
||||
SecretSyncs = "secret-syncs",
|
||||
Kmip = "kmip"
|
||||
Kmip = "kmip",
|
||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||
SecretScanningFindings = "secret-scanning-findings"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@@ -323,6 +340,11 @@ 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];
|
||||
|
||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||
|
||||
@@ -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,
|
||||
@@ -34,10 +36,15 @@ import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-con
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<
|
||||
AppConnection,
|
||||
{ name: string; image: string; size?: number }
|
||||
{ name: string; image: string; size?: number; icon?: IconDefinition }
|
||||
> = {
|
||||
[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"
|
||||
@@ -70,6 +77,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
return { name: "GitHub App", icon: faGithub };
|
||||
case GitHubRadarConnectionMethod.App:
|
||||
return { name: "GitHub App", icon: faGithub };
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -111,6 +116,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;
|
||||
};
|
||||
};
|
||||
@@ -9,6 +9,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";
|
||||
@@ -29,6 +30,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";
|
||||
@@ -43,6 +45,7 @@ export * from "./windmill-connection";
|
||||
export type TAppConnection =
|
||||
| TAwsConnection
|
||||
| TGitHubConnection
|
||||
| TGitHubRadarConnection
|
||||
| TGcpConnection
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
@@ -89,6 +92,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;
|
||||
|
||||
@@ -189,7 +189,18 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.ADD_IDENTITY_LDAP_AUTH]: "Attached LDAP Auth to identity",
|
||||
[EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity",
|
||||
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
|
||||
[EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked 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_SCAN]: "San Scanning Data Source",
|
||||
[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"
|
||||
};
|
||||
|
||||
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
|
||||
|
||||
@@ -183,5 +183,16 @@ export enum EventType {
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list"
|
||||
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_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"
|
||||
}
|
||||
|
||||
@@ -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,134 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretScanningV2Keys } from "./queries";
|
||||
import {
|
||||
TCreateSecretScanningDataSourceDTO,
|
||||
TDeleteSecretScanningDataSourceDTO,
|
||||
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)
|
||||
});
|
||||
// TODO: data source queries
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,218 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
TGetSecretScanningDataSource,
|
||||
TGetSecretScanningUnresolvedFindingsResponse,
|
||||
TListSecretScanningDataSourceOptions,
|
||||
TListSecretScanningDataSources,
|
||||
TListSecretScanningFindingsResponse,
|
||||
TListSecretScanningResourcesResponse,
|
||||
TListSecretScanningScansResponse,
|
||||
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]
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
};
|
||||
@@ -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,157 @@
|
||||
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 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";
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
};
|
||||
isAutoScanEnabled: boolean;
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
+13
-5
@@ -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,16 @@ export const ProjectLayout = () => {
|
||||
const pendingRequestsCount =
|
||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
|
||||
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(workspaceId, {
|
||||
enabled:
|
||||
isSecretScanning &&
|
||||
permission.can(
|
||||
ProjectPermissionSecretScanningFindingActions.Read,
|
||||
ProjectPermissionSub.SecretScanningFindings
|
||||
),
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
@@ -240,6 +256,41 @@ export const ProjectLayout = () => {
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
)}
|
||||
{isSecretScanning && (
|
||||
<Link
|
||||
to={`/${ProjectType.SecretScanning}/$projectId/data-sources` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
|
||||
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}
|
||||
|
||||
+9
@@ -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: {
|
||||
|
||||
+5
-1
@@ -18,6 +18,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";
|
||||
@@ -70,6 +71,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:
|
||||
@@ -144,6 +147,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:
|
||||
@@ -178,7 +183,6 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <TeamCityConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.OCI:
|
||||
return <OCIConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
|
||||
}
|
||||
|
||||
+157
@@ -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>
|
||||
);
|
||||
};
|
||||
+13
-5
@@ -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}
|
||||
|
||||
+75
-63
@@ -1,4 +1,4 @@
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Spinner, Tooltip } from "@app/components/v2";
|
||||
@@ -23,69 +23,81 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{appConnectionOptions?.map((option) => {
|
||||
const { image, name, size = 50 } = APP_CONNECTION_MAP[option.app];
|
||||
<>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{appConnectionOptions?.map((option) => {
|
||||
const { image, name, size = 50, icon } = APP_CONNECTION_MAP[option.app];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={option.app}
|
||||
onClick={() => onSelect(option.app)}
|
||||
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="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 connections.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
app 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">
|
||||
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
Coming Soon
|
||||
</div>
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={option.app}
|
||||
onClick={() => onSelect(option.app)}
|
||||
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"
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
style={{
|
||||
width: `${size}px`
|
||||
}}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
className="-right- 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>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center gap-1">
|
||||
<div className="max-w-xs text-center text-sm font-medium text-mineshaft-400 duration-200">
|
||||
Don't see the connection you're looking for?
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more connections.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
app 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>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon className="text-mineshaft-400" size="xs" icon={faInfoCircle} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+17
-6
@@ -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">
|
||||
|
||||
+56
@@ -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) {
|
||||
|
||||
+2
@@ -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.";
|
||||
};
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage";
|
||||
|
||||
export const SecretScanningOverviewPage = () => (
|
||||
<ProductOverviewPage type={ProjectType.SecretScanning} />
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { SecretScanningOverviewPage } from "./SecretScanningOverviewPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning/overview"
|
||||
)({
|
||||
component: SecretScanningOverviewPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Products",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />
|
||||
},
|
||||
{
|
||||
label: "Secret Scanning",
|
||||
link: linkOptions({ to: "/organization/secret-scanning/overview" })
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -1,221 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useServerConfig
|
||||
} from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePagination, usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateNewInstallationSession,
|
||||
useGetSecretScanningInstallationStatus,
|
||||
useGetSecretScanningRisks,
|
||||
useLinkGitAppInstallationWithOrg
|
||||
} from "@app/hooks/api/secretScanning";
|
||||
import { SecretScanningOrderBy } from "@app/hooks/api/secretScanning/types";
|
||||
|
||||
import { ExportSecretScansModal } from "./components/ExportSecretScansModal";
|
||||
import { SecretScanningFilter } from "./components/SecretScanningFilters";
|
||||
import { SecretScanningFilterFormData, secretScanningFilterFormSchema } from "./components/types";
|
||||
import { SecretScanningLogsTable } from "./components";
|
||||
|
||||
const PER_PAGE_INIT = 25;
|
||||
|
||||
export const SecretScanningPage = withPermission(
|
||||
() => {
|
||||
const queryParams = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SecretScanning.id
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { control, watch } = useForm<SecretScanningFilterFormData>({
|
||||
resolver: zodResolver(secretScanningFilterFormSchema),
|
||||
defaultValues: {}
|
||||
});
|
||||
|
||||
const { config } = useServerConfig();
|
||||
const { currentOrg } = useOrganization();
|
||||
const organizationId = currentOrg.id;
|
||||
|
||||
const { offset, limit, orderBy, setPage, perPage, page, setPerPage } = usePagination(
|
||||
SecretScanningOrderBy.CreatedAt,
|
||||
{ initPerPage: PER_PAGE_INIT }
|
||||
);
|
||||
|
||||
const repositoryNames = watch("repositoryNames");
|
||||
const resolvedStatus = watch("resolved");
|
||||
|
||||
const { isPending, data: risksData } = useGetSecretScanningRisks(
|
||||
organizationId,
|
||||
{
|
||||
offset,
|
||||
limit,
|
||||
orderBy
|
||||
},
|
||||
{
|
||||
repositoryNames:
|
||||
repositoryNames?.length > 0 ? repositoryNames.map((repo) => repo.name) : undefined,
|
||||
resolvedStatus
|
||||
}
|
||||
);
|
||||
|
||||
const { mutateAsync: linkGitAppInstallationWithOrganization } =
|
||||
useLinkGitAppInstallationWithOrg();
|
||||
const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession();
|
||||
const {
|
||||
data: installationStatus,
|
||||
isPending: isSecretScanningInstatllationStatusLoading,
|
||||
refetch: refetchSecretScanningInstallationStatus
|
||||
} = useGetSecretScanningInstallationStatus(organizationId);
|
||||
const integrationEnabled =
|
||||
!isSecretScanningInstatllationStatusLoading && installationStatus?.appInstallationCompleted;
|
||||
|
||||
const { handlePopUpToggle, popUp } = usePopUp(["exportSecretScans"]);
|
||||
|
||||
useEffect(() => {
|
||||
const linkInstallation = async () => {
|
||||
if (queryParams.state && queryParams.installation_id) {
|
||||
try {
|
||||
const isLinked = await linkGitAppInstallationWithOrganization({
|
||||
installationId: String(queryParams.installation_id),
|
||||
sessionId: String(queryParams.state)
|
||||
});
|
||||
if (isLinked) {
|
||||
await navigate({
|
||||
to: "/organization/secret-scanning"
|
||||
});
|
||||
refetchSecretScanningInstallationStatus();
|
||||
}
|
||||
|
||||
console.log("installation verification complete");
|
||||
} catch (e) {
|
||||
console.log("app installation is stale, start new session", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
linkInstallation();
|
||||
}, [queryParams.state, queryParams.installation_id]);
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession({ organizationId });
|
||||
window.location.href = `https://github.com/apps/${session.gitAppSlug}/installations/new?state=${session.sessionId}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<Helmet>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Secret Scanning"
|
||||
description="Automatically monitor your GitHub activity and prevent secret leaks"
|
||||
/>
|
||||
{config.isSecretScanningDisabled && (
|
||||
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
|
||||
We are working on improving the performance of secret scanning due to increased
|
||||
usage.
|
||||
</NoticeBanner>
|
||||
)}
|
||||
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mb-1 flex flex-row">
|
||||
Secret Scanning Status:{" "}
|
||||
{integrationEnabled ? (
|
||||
<p className="ml-1.5 font-semibold text-green">Enabled</p>
|
||||
) : (
|
||||
<p className="ml-1.5 font-semibold text-red">Not enabled</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{integrationEnabled ? (
|
||||
<p className="text-mineshaft-300">
|
||||
Your GitHub organization is connected to Infisical, and is being continuously
|
||||
monitored for secret leaks.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-mineshaft-300">
|
||||
Connect your GitHub organization to Infisical.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{integrationEnabled ? (
|
||||
<div>
|
||||
<div className="absolute right-[2.5rem] top-[2.5rem] flex h-6 w-6 animate-ping items-center justify-center rounded-full bg-green" />
|
||||
<div className="absolute right-[2.63rem] top-[2.63rem] flex h-5 w-5 animate-ping items-center justify-center rounded-full bg-green" />
|
||||
<div className="absolute right-[2.82rem] top-[2.82rem] flex h-3.5 w-3.5 animate-ping items-center justify-center rounded-full bg-green" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[3.25rem] items-center">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.SecretScanning}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="primary"
|
||||
onClick={generateNewIntegrationSession}
|
||||
className="h-min py-2"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Integrate with GitHub
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
{integrationEnabled && (
|
||||
<div className="flex w-full items-center justify-end">
|
||||
<SecretScanningFilter
|
||||
repositories={risksData?.repos || []}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<SecretScanningLogsTable gitRisks={risksData?.risks} isPending={isPending} />
|
||||
{!isPending &&
|
||||
risksData?.totalCount !== undefined &&
|
||||
risksData.totalCount >= PER_PAGE_INIT && (
|
||||
<Pagination
|
||||
className="rounded-md"
|
||||
count={risksData.totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ExportSecretScansModal
|
||||
repositories={risksData?.repos || []}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
popUp={popUp}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionActions.Read,
|
||||
subject: OrgPermissionSubjects.SecretScanning
|
||||
}
|
||||
);
|
||||
-200
@@ -1,200 +0,0 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import FileSaver from "file-saver";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useExportSecretScanningRisks } from "@app/hooks/api/secretScanning";
|
||||
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { convertJsonToCsv } from "@app/lib/fn/csv";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["exportSecretScans"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["exportSecretScans"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
repositories: string[];
|
||||
};
|
||||
|
||||
enum ExportFormat {
|
||||
Json = "json",
|
||||
Csv = "csv"
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
githubRepository: z.string().trim(),
|
||||
status: z.nativeEnum(SecretScanningResolvedStatus),
|
||||
exportFormat: z.nativeEnum(ExportFormat)
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, repositories }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync } = useExportSecretScanningRisks();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
exportFormat: ExportFormat.Json,
|
||||
status: SecretScanningResolvedStatus.All,
|
||||
githubRepository: "all"
|
||||
}
|
||||
});
|
||||
|
||||
const onFormSubmit = async (data: TFormSchema) => {
|
||||
const gitRisks = await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
filter: {
|
||||
repositoryNames: data.githubRepository !== "all" ? [data.githubRepository] : undefined,
|
||||
resolvedStatus: data.status
|
||||
}
|
||||
});
|
||||
|
||||
const filteredRisks = gitRisks
|
||||
.filter((risk) =>
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
data.status === SecretScanningResolvedStatus.All
|
||||
? true
|
||||
: data.status === SecretScanningResolvedStatus.Resolved
|
||||
? risk.isResolved
|
||||
: !risk.isResolved
|
||||
)
|
||||
.filter((risk) => {
|
||||
if (data.githubRepository === "all") return true;
|
||||
return risk.repositoryFullName === data.githubRepository;
|
||||
});
|
||||
|
||||
const formattedRisks = filteredRisks.map((risk) => {
|
||||
return {
|
||||
repository: risk.repositoryFullName,
|
||||
fileName: risk.file,
|
||||
isResolved: risk.isResolved ? "Resolved" : "Needs Attention",
|
||||
status: risk.status || "UNRESOLVED",
|
||||
entropy: risk.entropy,
|
||||
secretType: risk.ruleID,
|
||||
exposedSecretLink: `https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`,
|
||||
commitAuthor: risk.author,
|
||||
commitAuthorEmail: risk.email,
|
||||
commitDate: risk.createdAt,
|
||||
commitMessage: risk.message,
|
||||
commitFingerprint: risk.fingerprint
|
||||
};
|
||||
});
|
||||
|
||||
const fileName = `infisical-secret-scans-${new Date().toISOString()}`;
|
||||
if (data.exportFormat === ExportFormat.Csv) {
|
||||
const csvBlob = convertJsonToCsv(formattedRisks);
|
||||
FileSaver.saveAs(csvBlob, `${fileName}.csv`);
|
||||
} else if (data.exportFormat === ExportFormat.Json) {
|
||||
const json = JSON.stringify(formattedRisks, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
FileSaver.saveAs(blob, `${fileName}.json`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Modal
|
||||
isOpen={popUp?.exportSecretScans?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("exportSecretScans", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Export Secret Scans"
|
||||
footerContent={[
|
||||
<Button
|
||||
onClick={() => handleSubmit(onFormSubmit)()}
|
||||
isLoading={isSubmitting}
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
>
|
||||
Export secret scans
|
||||
</Button>,
|
||||
<Button
|
||||
key="keep-old-btn"
|
||||
className="ml-4"
|
||||
onClick={() => handlePopUpToggle("exportSecretScans", false)}
|
||||
variant="outline_bg"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="exportFormat"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Export Format">
|
||||
<Select
|
||||
defaultValue={ExportFormat.Json}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full"
|
||||
>
|
||||
{Object.values(ExportFormat).map((format) => (
|
||||
<SelectItem key={format} value={format}>
|
||||
{format.toUpperCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="status"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Risk Status">
|
||||
<Select
|
||||
defaultValue="all"
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full"
|
||||
>
|
||||
{Object.values(SecretScanningResolvedStatus).map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="githubRepository"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="GitHub Repository">
|
||||
<Select
|
||||
defaultValue="all"
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="all">All Repositories</SelectItem>
|
||||
{repositories.map((repoName) => (
|
||||
<SelectItem key={repoName} value={repoName}>
|
||||
{repoName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useUpdateRiskStatus } from "@app/hooks/api/secretScanning";
|
||||
import { RiskStatus } from "@app/hooks/api/secretScanning/types";
|
||||
|
||||
export const RiskStatusSelection = ({
|
||||
riskId,
|
||||
currentSelection
|
||||
}: {
|
||||
riskId: any;
|
||||
currentSelection: any;
|
||||
}) => {
|
||||
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
|
||||
const { mutate: updateRiskStatus } = useUpdateRiskStatus();
|
||||
const { currentOrg } = useOrganization();
|
||||
const organizationId = currentOrg.id;
|
||||
|
||||
return (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.SecretScanning}>
|
||||
{(isAllowed) => (
|
||||
<select
|
||||
disabled={!isAllowed}
|
||||
value={selectedRiskStatus}
|
||||
onChange={(e) => {
|
||||
setSelectedRiskStatus(e.target.value);
|
||||
updateRiskStatus({
|
||||
organizationId,
|
||||
status: e.target.value as RiskStatus,
|
||||
riskId
|
||||
});
|
||||
}}
|
||||
className="block w-full rounded-md px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
|
||||
>
|
||||
<option>Unresolved</option>
|
||||
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>
|
||||
This is a false positive, resolved
|
||||
</option>
|
||||
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
|
||||
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
|
||||
</select>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
);
|
||||
};
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SecretScanningFilterFormData } from "./types";
|
||||
|
||||
type Props = {
|
||||
control: Control<SecretScanningFilterFormData>;
|
||||
repositories: string[];
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["exportSecretScans"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const SecretScanningFilter = ({ repositories, control, handlePopUpToggle }: Props) => {
|
||||
return (
|
||||
<div className={twMerge("flex w-full flex-wrap items-center justify-between bg-bunker-800")}>
|
||||
<div className="flex items-center -space-x-8">
|
||||
<Controller
|
||||
control={control}
|
||||
name="repositoryNames"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Repository"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-12 w-96"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
isMulti
|
||||
onChange={onChange}
|
||||
placeholder="Select a repository..."
|
||||
options={repositories.map((repository) => ({
|
||||
name: repository
|
||||
}))}
|
||||
getOptionValue={(option) => option.name}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="resolved"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Status"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-12 w-44"
|
||||
>
|
||||
<Select
|
||||
defaultValue={SecretScanningResolvedStatus.All}
|
||||
placeholder={SecretScanningResolvedStatus.All}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full"
|
||||
>
|
||||
{Object.values(SecretScanningResolvedStatus).map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)} risks
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
className="mt-[0.45rem]"
|
||||
onClick={() => handlePopUpToggle("exportSecretScans", true)}
|
||||
variant="solid"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { formatDistance } from "date-fns";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { TSecretScanningGitRisks } from "@app/hooks/api/secretScanning/types";
|
||||
|
||||
import { RiskStatusSelection } from "./RiskStatusSelection";
|
||||
|
||||
type Props = {
|
||||
gitRisks?: TSecretScanningGitRisks[];
|
||||
isPending: boolean;
|
||||
};
|
||||
|
||||
export const SecretScanningLogsTable = ({ gitRisks, isPending }: Props) => {
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Date</Th>
|
||||
<Th className="flex-1">Secret Type</Th>
|
||||
<Th className="flex-1">View Risk</Th>
|
||||
<Th className="flex-1">Info</Th>
|
||||
<Th className="flex-1">Status</Th>
|
||||
<Th className="flex-1">Action</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
gitRisks &&
|
||||
gitRisks?.map((risk) => {
|
||||
return (
|
||||
<Tr key={risk.ruleID} className="h-10">
|
||||
<Td>{formatDistance(new Date(risk.createdAt), new Date())}</Td>
|
||||
<Td>{risk.ruleID}</Td>
|
||||
<Td>
|
||||
<a
|
||||
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
||||
target="_blank"
|
||||
className="text-red-500"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View Exposed Secret
|
||||
</a>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="font-bold">
|
||||
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
||||
{risk.repositoryFullName}
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span>{risk.file}</span>
|
||||
<br />
|
||||
<br />
|
||||
<span className="font-bold">{risk.author}</span>
|
||||
<br />
|
||||
<span>{risk.email}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
|
||||
<Td>
|
||||
<RiskStatusSelection riskId={risk.id} currentSelection={risk.status} />
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isPending && <TableSkeleton columns={7} innerKey="gitRisks" />}
|
||||
{!isPending && gitRisks && gitRisks?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={7}>
|
||||
<EmptyState title="No risks detected." icon={faCheck} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { SecretScanningLogsTable } from "./SecretScanningLogsTable";
|
||||
@@ -1,22 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
|
||||
|
||||
export const secretScanningFilterFormSchema = z.object({
|
||||
repositoryNames: z.array(z.object({ name: z.string() })),
|
||||
resolved: z
|
||||
.nativeEnum(SecretScanningResolvedStatus)
|
||||
.default(SecretScanningResolvedStatus.All)
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type SecretScanningFilterFormData = z.infer<typeof secretScanningFilterFormSchema>;
|
||||
|
||||
export type SetValueType = (
|
||||
name: keyof SecretScanningFilterFormData,
|
||||
value: any,
|
||||
options?: {
|
||||
shouldValidate?: boolean;
|
||||
shouldDirty?: boolean;
|
||||
}
|
||||
) => void;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningPage } from "./SecretScanningPage";
|
||||
|
||||
const SecretScanningQueryParams = z.object({
|
||||
state: z.string().catch(""),
|
||||
installation_id: z.coerce.string().optional()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
||||
)({
|
||||
validateSearch: zodValidator(SecretScanningQueryParams),
|
||||
component: SecretScanningPage,
|
||||
search: {
|
||||
middlewares: [
|
||||
stripSearchParams({
|
||||
installation_id: "",
|
||||
state: ""
|
||||
})
|
||||
]
|
||||
},
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Home",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />,
|
||||
link: linkOptions({ to: "/" })
|
||||
},
|
||||
{
|
||||
label: "Secret Scanning"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
export const SecretScanningSettingsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Secret Scanning Settings</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Secret Scanning Settings" />
|
||||
<ProjectSettings />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { SecretScanningSettingsPage } from "./SecretScanningSettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning/settings"
|
||||
)({
|
||||
component: SecretScanningSettingsPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Products",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />
|
||||
},
|
||||
{
|
||||
label: "Secret Scanning",
|
||||
link: linkOptions({ to: "/organization/secret-scanning/settings" })
|
||||
},
|
||||
{
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { AccessControlPage } from "./AccessControlPage";
|
||||
|
||||
const AccessControlPageQuerySchema = z.object({
|
||||
selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member),
|
||||
requesterEmail: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/access-management"
|
||||
)({
|
||||
component: AccessControlPage,
|
||||
validateSearch: zodValidator(AccessControlPageQuerySchema),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ requesterEmail: "" })]
|
||||
},
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/secret-scanning/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId"
|
||||
)({
|
||||
component: IdentityDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/secret-scanning/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Identities"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId"
|
||||
)({
|
||||
component: MemberDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/secret-scanning/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Users"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
+125
-4
@@ -20,6 +20,8 @@ import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretScanningDataSourceActions,
|
||||
ProjectPermissionSecretScanningFindingActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
ProjectPermissionSshHostActions,
|
||||
TPermissionCondition,
|
||||
@@ -90,6 +92,21 @@ const SecretRotationPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretRotationActions.RotateSecrets]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretScanningDataSourcePolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretScanningDataSourceActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretScanningDataSourceActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretScanningDataSourceActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretScanningDataSourceActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretScanningDataSourceActions.ReadScans]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretScanningDataSourceActions.ReadResources]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretScanningDataSourceActions.TriggerScans]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretScanningFindingPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretScanningFindingActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionSecretScanningFindingActions.Resolve]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const KmipPolicyActionSchema = z.object({
|
||||
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
|
||||
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
|
||||
@@ -274,7 +291,11 @@ export const projectRoleFormSchema = z.object({
|
||||
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([])
|
||||
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretScanningDataSources]:
|
||||
SecretScanningDataSourcePolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretScanningFindings]:
|
||||
SecretScanningFindingPolicyActionSchema.array().default([])
|
||||
})
|
||||
.partial()
|
||||
.optional()
|
||||
@@ -1250,6 +1271,52 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
value: ProjectPermissionKmipActions.GenerateClientCertificates
|
||||
}
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SecretScanningDataSources]: {
|
||||
title: "Secret Scanning Data Sources",
|
||||
actions: [
|
||||
{
|
||||
label: "Read Data Sources",
|
||||
value: ProjectPermissionSecretScanningDataSourceActions.Read
|
||||
},
|
||||
{
|
||||
label: "Create Data Sources",
|
||||
value: ProjectPermissionSecretScanningDataSourceActions.Create
|
||||
},
|
||||
{
|
||||
label: "Modify Data Sources",
|
||||
value: ProjectPermissionSecretScanningDataSourceActions.Edit
|
||||
},
|
||||
{
|
||||
label: "Delete Data Sources",
|
||||
value: ProjectPermissionSecretScanningDataSourceActions.Delete
|
||||
},
|
||||
{
|
||||
label: "Read Resources",
|
||||
value: ProjectPermissionSecretScanningDataSourceActions.ReadResources
|
||||
},
|
||||
{
|
||||
label: "Read Scans",
|
||||
value: ProjectPermissionSecretScanningDataSourceActions.ReadScans
|
||||
},
|
||||
{
|
||||
label: "Trigger Scans",
|
||||
value: ProjectPermissionSecretScanningDataSourceActions.TriggerScans
|
||||
}
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SecretScanningFindings]: {
|
||||
title: "Secret Scanning Findings",
|
||||
actions: [
|
||||
{
|
||||
label: "Read Findings",
|
||||
value: ProjectPermissionSecretScanningFindingActions.Read
|
||||
},
|
||||
{
|
||||
label: "Resolve Finding Status",
|
||||
value: ProjectPermissionSecretScanningFindingActions.Resolve
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1303,6 +1370,11 @@ const SshPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.SshHostGroups]: enabled
|
||||
});
|
||||
|
||||
const SecretScanningSubject = (enabled = false) => ({
|
||||
[ProjectPermissionSub.SecretScanningDataSources]: enabled,
|
||||
[ProjectPermissionSub.SecretScanningFindings]: enabled
|
||||
});
|
||||
|
||||
// scott: this structure ensures we don't forget to add project permissions to their relevant project type
|
||||
export const ProjectTypePermissionSubjects: Record<
|
||||
ProjectType,
|
||||
@@ -1313,27 +1385,39 @@ export const ProjectTypePermissionSubjects: Record<
|
||||
...SecretsManagerPermissionSubjects(true),
|
||||
...KmsPermissionSubjects(),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
...SshPermissionSubjects(),
|
||||
...SecretScanningSubject()
|
||||
},
|
||||
[ProjectType.KMS]: {
|
||||
...SharedPermissionSubjects,
|
||||
...KmsPermissionSubjects(true),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
...SshPermissionSubjects(),
|
||||
...SecretScanningSubject()
|
||||
},
|
||||
[ProjectType.CertificateManager]: {
|
||||
...SharedPermissionSubjects,
|
||||
...CertificateManagerPermissionSubjects(true),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...SshPermissionSubjects()
|
||||
...SshPermissionSubjects(),
|
||||
...SecretScanningSubject()
|
||||
},
|
||||
[ProjectType.SSH]: {
|
||||
...SharedPermissionSubjects,
|
||||
...SshPermissionSubjects(true),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects(),
|
||||
...SecretScanningSubject()
|
||||
},
|
||||
[ProjectType.SecretScanning]: {
|
||||
...SharedPermissionSubjects,
|
||||
...SecretScanningSubject(true),
|
||||
...SshPermissionSubjects(),
|
||||
...CertificateManagerPermissionSubjects(),
|
||||
...KmsPermissionSubjects(),
|
||||
...SecretsManagerPermissionSubjects()
|
||||
}
|
||||
};
|
||||
@@ -1546,6 +1630,43 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
||||
},
|
||||
projectManagerTemplate()
|
||||
],
|
||||
[ProjectType.SecretScanning]: [
|
||||
{
|
||||
id: "scanning-viewer",
|
||||
name: "Secret Scanning Viewing Policies",
|
||||
description: "Grants read access to data sources and findings",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretScanningDataSources,
|
||||
actions: [
|
||||
ProjectPermissionSecretScanningDataSourceActions.Read,
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadResources,
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadScans
|
||||
]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretScanningFindings,
|
||||
actions: [ProjectPermissionSecretScanningFindingActions.Read]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "scanning-editor",
|
||||
name: "Secret Scanning Editing Policies",
|
||||
description: "Grants read and edit access to data sources and findings",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretScanningDataSources,
|
||||
actions: Object.values(ProjectPermissionSecretScanningDataSourceActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretScanningFindings,
|
||||
actions: Object.values(ProjectPermissionSecretScanningFindingActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate()
|
||||
],
|
||||
[ProjectType.SecretManager]: [
|
||||
{
|
||||
id: "secret-viewer",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug"
|
||||
)({
|
||||
component: RoleDetailsBySlugPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/secret-scanning/$projectId/access-management",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Roles"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -252,7 +252,7 @@ export const SecretSyncRow = ({
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={(e) => {
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faBan, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, ContentLoader, EmptyState } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
useGetSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import {
|
||||
SecretScanningDataSourceSection,
|
||||
SecretScanningResourceSection,
|
||||
SecretScanningScanSection
|
||||
} from "./components";
|
||||
|
||||
const PageContent = () => {
|
||||
const navigate = useNavigate();
|
||||
const { type, dataSourceId, projectId } = useParams({
|
||||
from: ROUTE_PATHS.SecretScanning.DataSourceByIdPage.id,
|
||||
select: (params) => ({
|
||||
...params,
|
||||
type: params.type as SecretScanningDataSource
|
||||
})
|
||||
});
|
||||
|
||||
const { data: dataSource, isPending } = useGetSecretScanningDataSource({ type, dataSourceId });
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<ContentLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const details = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
if (!dataSource) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center px-20">
|
||||
<EmptyState
|
||||
className="max-w-2xl rounded-md text-center"
|
||||
icon={faBan}
|
||||
title={`Could not find ${details.name ?? "Secret Scanning"} Data Source with ID ${dataSourceId}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
onClick={() => {
|
||||
navigate({
|
||||
to: "/secret-scanning/$projectId/data-sources",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Data Sources
|
||||
</Button>
|
||||
<div className="mb-6 flex w-full items-center gap-3">
|
||||
<img
|
||||
alt={`${details.name} data source`}
|
||||
src={`/images/integrations/${details.image}`}
|
||||
className="ml-1 mt-3 w-14"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl font-semibold text-white">{dataSource.name}</p>
|
||||
<p className="leading-3 text-bunker-300">{details.name} Data Source</p>
|
||||
</div>
|
||||
{/* <SecretSyncActionTriggers dataSource={dataSource} /> */}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||
<SecretScanningDataSourceSection dataSource={dataSource} />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<SecretScanningResourceSection dataSource={dataSource} />
|
||||
<SecretScanningScanSection dataSource={dataSource} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceByIdPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Secret Scanning Data Source | Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
passThrough={false}
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.Read}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
<PageContent />
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
TSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay";
|
||||
|
||||
type Props = {
|
||||
dataSource: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
export const DataSourceConfigDisplay = ({ dataSource }: Props) => {
|
||||
switch (dataSource.type) {
|
||||
case SecretScanningDataSource.GitHub:
|
||||
return <GitHubDataSourceConfigDisplay dataSource={dataSource} />;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}`
|
||||
);
|
||||
}
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { TGitHubDataSource } from "@app/hooks/api/secretScanningV2/types/github-data-source";
|
||||
|
||||
type Props = {
|
||||
dataSource: TGitHubDataSource;
|
||||
};
|
||||
|
||||
export const GitHubDataSourceConfigDisplay = ({ dataSource }: Props) => {
|
||||
const {
|
||||
config: { includeRepos }
|
||||
} = dataSource;
|
||||
|
||||
return (
|
||||
<GenericFieldLabel label="Scan Repositories">
|
||||
{includeRepos.includes("*") ? "All" : includeRepos.join(", ")}
|
||||
</GenericFieldLabel>
|
||||
);
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { faBan, faCheck, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { EditSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { Badge, IconButton } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
import { DataSourceConfigDisplay } from "@app/pages/secret-scanning/SecretScanningDataSourceByIdPage/components/DataSourceConfigDisplay/DataSourceConfigDisplay";
|
||||
|
||||
type Props = {
|
||||
dataSource: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceSection = ({ dataSource }: Props) => {
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["editDataSource"] as const);
|
||||
|
||||
const { name, description, connection, isAutoScanEnabled } = dataSource;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Details</h3>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.Edit}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="Edit data source"
|
||||
onClick={() => handlePopUpOpen("editDataSource")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
|
||||
{connection && (
|
||||
<GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>
|
||||
)}
|
||||
<DataSourceConfigDisplay dataSource={dataSource} />
|
||||
<GenericFieldLabel label="Auto-Scan">
|
||||
{isAutoScanEnabled ? (
|
||||
<Badge
|
||||
variant="success"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
<span>Enabled</span>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
<span>Disabled</span>
|
||||
</Badge>
|
||||
)}
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EditSecretScanningDataSourceModal
|
||||
dataSource={dataSource}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editDataSource", isOpen)}
|
||||
isOpen={popUp.editDataSource.isOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
faCheck,
|
||||
faCopy,
|
||||
faEllipsisV,
|
||||
faExpand,
|
||||
faInfoCircle,
|
||||
faSearch,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretScanningScanStatusBadge } from "@app/components/secret-scanning";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionSecretScanningDataSourceActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
SecretScanningFindingStatus,
|
||||
SecretScanningScanStatus,
|
||||
TSecretScanningDataSource,
|
||||
TSecretScanningResourceWithDetails,
|
||||
useTriggerSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
resource: TSecretScanningResourceWithDetails;
|
||||
dataSource: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => {
|
||||
const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } =
|
||||
resource;
|
||||
|
||||
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleTriggerScan = async () => {
|
||||
try {
|
||||
await triggerDataSourceScan.mutateAsync({
|
||||
dataSourceId: dataSource.id,
|
||||
type: dataSource.type,
|
||||
projectId: dataSource.projectId,
|
||||
resourceId: id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully triggered scan for ${name}`,
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to trigger scan for ${name}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||
|
||||
const handleCopyId = useCallback(
|
||||
(idToCopy: string) => {
|
||||
setIsIdCopied.on();
|
||||
navigator.clipboard.writeText(idToCopy);
|
||||
|
||||
createNotification({
|
||||
text: "Resource ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
},
|
||||
[isIdCopied]
|
||||
);
|
||||
|
||||
const resourceDetails = RESOURCE_DESCRIPTION_HELPER[dataSource.type];
|
||||
|
||||
return (
|
||||
<Tr
|
||||
className={twMerge(
|
||||
"group h-10 transition-colors duration-100 hover:bg-mineshaft-700",
|
||||
lastScanStatus === SecretScanningScanStatus.Failed && "bg-red/5 hover:bg-red/10"
|
||||
)}
|
||||
key={`resource-${id}`}
|
||||
>
|
||||
<Td className="!min-w-[8rem] max-w-0">
|
||||
<div className="flex w-full items-center">
|
||||
<p className="truncate">{name}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{lastScanStatus?.match(/queued|scanning/) ? (
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="flex h-5 w-min animate-pulse items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
<span>Scanning For Leaks</span>
|
||||
</Badge>
|
||||
) : // eslint-disable-next-line no-nested-ternary
|
||||
lastScannedAt ? (
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
unresolvedFindings ? (
|
||||
<Badge
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/secret-scanning/$projectId/findings",
|
||||
params: {
|
||||
projectId: dataSource.projectId
|
||||
},
|
||||
search: {
|
||||
search: name,
|
||||
status: SecretScanningFindingStatus.Unresolved
|
||||
}
|
||||
})
|
||||
}
|
||||
variant="primary"
|
||||
className="flex h-5 w-min cursor-pointer items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} />
|
||||
<span>
|
||||
{unresolvedFindings} Secret{unresolvedFindings > 1 ? "s" : ""} Detected
|
||||
</span>
|
||||
</Badge>
|
||||
) : lastScanStatus === SecretScanningScanStatus.Failed ? (
|
||||
<span className="text-mineshaft-400">No findings</span>
|
||||
) : (
|
||||
<Badge
|
||||
variant="success"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
No Secrets Detected
|
||||
</Badge>
|
||||
)
|
||||
) : (
|
||||
<span className="text-mineshaft-400">No findings</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="whitespace-nowrap">
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{lastScanStatus?.match(/queued|scanning|failed/) ? (
|
||||
<SecretScanningScanStatusBadge
|
||||
status={lastScanStatus}
|
||||
statusMessage={lastScanStatusMessage}
|
||||
scannedAt={lastScannedAt}
|
||||
/>
|
||||
) : lastScannedAt ? (
|
||||
formatDistance(new Date(lastScannedAt), new Date(), { addSuffix: true })
|
||||
) : (
|
||||
<span className="text-mineshaft-400">No scans</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyId(id);
|
||||
}}
|
||||
>
|
||||
Copy Resource ID
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.TriggerScans}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faExpand} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleTriggerScan();
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
position="left"
|
||||
sideOffset={42}
|
||||
content={`Manually trigger a scan for this ${resourceDetails.singularNoun}.`}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-between gap-1">
|
||||
<span> Trigger Scan</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-bunker-300"
|
||||
size="sm"
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { faExpand } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import {
|
||||
RESOURCE_DESCRIPTION_HELPER,
|
||||
SECRET_SCANNING_DATA_SOURCE_MAP
|
||||
} from "@app/helpers/secretScanningV2";
|
||||
import {
|
||||
TSecretScanningDataSource,
|
||||
useTriggerSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningResourcesTable } from "./SecretScanningResourceTable";
|
||||
|
||||
type Props = {
|
||||
dataSource: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
export const SecretScanningResourceSection = ({ dataSource }: Props) => {
|
||||
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
|
||||
|
||||
const handleTriggerScan = async () => {
|
||||
try {
|
||||
await triggerDataSourceScan.mutateAsync({
|
||||
dataSourceId: dataSource.id,
|
||||
type: dataSource.type,
|
||||
projectId: dataSource.projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully triggered scan for ${dataSource.name}`,
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to trigger scan for ${dataSource.name}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resourceDetails = RESOURCE_DESCRIPTION_HELPER[dataSource.type];
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xl font-semibold text-mineshaft-100">{resourceDetails.pluralTitle}</p>
|
||||
<p className="text-sm text-bunker-300">
|
||||
{resourceDetails.pluralTitle} associated with this{" "}
|
||||
{SECRET_SCANNING_DATA_SOURCE_MAP[dataSource.type].name} Data Source
|
||||
</p>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.TriggerScans}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faExpand} />}
|
||||
onClick={handleTriggerScan}
|
||||
isDisabled={!isAllowed || triggerDataSourceScan.isPending}
|
||||
isLoading={triggerDataSourceScan.isPending}
|
||||
>
|
||||
Scan {resourceDetails.pluralTitle}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<SecretScanningResourcesTable dataSource={dataSource} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import {
|
||||
SecretScanningScanStatus,
|
||||
TSecretScanningDataSource,
|
||||
useListSecretScanningResources
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningResourceRow } from "./SecretScanningResourceRow";
|
||||
|
||||
enum ResourcesOrderBy {
|
||||
Name = "name",
|
||||
Findings = "findings",
|
||||
LastScanned = "last-scanned"
|
||||
}
|
||||
|
||||
type Props = {
|
||||
dataSource: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const canReadResources = permission.can(
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadResources,
|
||||
ProjectPermissionSub.SecretScanningDataSources
|
||||
);
|
||||
|
||||
const { data: resources = [], isPending: isResourcesPending } = useListSecretScanningResources(
|
||||
{ dataSourceId: dataSource.id, type: dataSource.type },
|
||||
{
|
||||
refetchInterval: 30000,
|
||||
enabled: canReadResources
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<ResourcesOrderBy>(ResourcesOrderBy.Name, { initPerPage: 10 });
|
||||
|
||||
const filteredResources = useMemo(
|
||||
() =>
|
||||
resources
|
||||
.filter((resource) => {
|
||||
const { name } = resource;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return name.toLowerCase().includes(searchValue);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [resourceOne, resourceTwo] =
|
||||
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case ResourcesOrderBy.Findings:
|
||||
if (
|
||||
resourceOne.unresolvedFindings === 0 &&
|
||||
resourceOne.lastScanStatus === SecretScanningScanStatus.Failed
|
||||
)
|
||||
return 1;
|
||||
if (
|
||||
resourceTwo.unresolvedFindings === 0 &&
|
||||
resourceTwo.lastScanStatus === SecretScanningScanStatus.Failed
|
||||
)
|
||||
return -1;
|
||||
|
||||
return resourceTwo.unresolvedFindings - resourceOne.unresolvedFindings;
|
||||
case ResourcesOrderBy.LastScanned:
|
||||
return (
|
||||
new Date(resourceTwo.lastScannedAt).getTime() -
|
||||
new Date(resourceOne.lastScannedAt).getTime()
|
||||
);
|
||||
case ResourcesOrderBy.Name:
|
||||
default:
|
||||
return resourceOne.name.toLowerCase().localeCompare(resourceTwo.name.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[resources, orderDirection, search, orderBy]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredResources.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: ResourcesOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: ResourcesOrderBy) =>
|
||||
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: ResourcesOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
const resourceDetails = RESOURCE_DESCRIPTION_HELPER[dataSource.type];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder={`Search ${resourceDetails.pluralNoun}...`}
|
||||
className="flex-1"
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/2">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(ResourcesOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(ResourcesOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(ResourcesOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/4">
|
||||
<div className="flex items-center">
|
||||
Findings
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(ResourcesOrderBy.Findings)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(ResourcesOrderBy.Findings)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(ResourcesOrderBy.Findings)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
Last Scan
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(ResourcesOrderBy.LastScanned)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(ResourcesOrderBy.LastScanned)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(ResourcesOrderBy.LastScanned)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{canReadResources && isResourcesPending && (
|
||||
<TableSkeleton columns={5} innerKey="resource" />
|
||||
)}
|
||||
{filteredResources.slice(offset, perPage * page).map((resource) => (
|
||||
<SecretScanningResourceRow
|
||||
key={resource.id}
|
||||
resource={resource}
|
||||
dataSource={dataSource}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!canReadResources && (
|
||||
<EmptyState
|
||||
icon={faBan}
|
||||
title={`You do not have permission to view data source ${resourceDetails.pluralNoun}`}
|
||||
/>
|
||||
)}
|
||||
{Boolean(filteredResources.length) && (
|
||||
<Pagination
|
||||
count={filteredResources.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{!isResourcesPending && !filteredResources?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
resources.length
|
||||
? "No resources match search..."
|
||||
: `This data source has no ${resourceDetails.pluralNoun} associated with it.`
|
||||
}
|
||||
icon={resources.length ? faSearch : undefined}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy, faEllipsisV, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretScanningScanStatusBadge } from "@app/components/secret-scanning";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
SecretScanningScanStatus,
|
||||
SecretScanningScanType,
|
||||
TSecretScanningScanWithDetails
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
scan: TSecretScanningScanWithDetails;
|
||||
};
|
||||
|
||||
export const SecretScanningScanRow = ({ scan }: Props) => {
|
||||
const {
|
||||
id,
|
||||
resourceName,
|
||||
createdAt,
|
||||
status,
|
||||
statusMessage,
|
||||
unresolvedFindings,
|
||||
resolvedFindings,
|
||||
type
|
||||
} = scan;
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const totalFindings = resolvedFindings + unresolvedFindings;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||
|
||||
const handleCopyId = useCallback(() => {
|
||||
setIsIdCopied.on();
|
||||
navigator.clipboard.writeText(id);
|
||||
|
||||
createNotification({
|
||||
text: "Scan ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isIdCopied]);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
className={twMerge(
|
||||
"group h-10 transition-colors duration-100 hover:bg-mineshaft-700",
|
||||
status === SecretScanningScanStatus.Failed && "bg-red/5 hover:bg-red/10"
|
||||
)}
|
||||
key={`scan-${id}`}
|
||||
>
|
||||
<Td>
|
||||
<p>
|
||||
{format(createdAt, "MMM dd yyyy")}{" "}
|
||||
<span className="text-mineshaft-300">{format(createdAt, "h:mm aa")}</span>
|
||||
</p>
|
||||
</Td>
|
||||
<Td className="!min-w-[8rem] max-w-0">
|
||||
<div className="flex flex-col">
|
||||
<p className="truncate">{resourceName}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="whitespace-nowrap">
|
||||
{type === SecretScanningScanType.FullScan ? "Full scan" : "Diff scan"}
|
||||
</Td>
|
||||
<Td>
|
||||
{
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
status?.match(/queued|scanning|failed/) ? (
|
||||
<SecretScanningScanStatusBadge status={status} statusMessage={statusMessage} />
|
||||
) : // eslint-disable-next-line no-nested-ternary
|
||||
totalFindings ? (
|
||||
<div className="flex flex-col">
|
||||
<Badge
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/secret-scanning/$projectId/findings",
|
||||
params: {
|
||||
projectId: currentWorkspace.id
|
||||
},
|
||||
search: {
|
||||
search: `scanId:${id}`
|
||||
}
|
||||
})
|
||||
}
|
||||
variant={unresolvedFindings ? "primary" : undefined}
|
||||
className={twMerge(
|
||||
"flex h-5 w-min cursor-pointer items-center gap-1.5 whitespace-nowrap",
|
||||
!unresolvedFindings && "bg-mineshaft-400/50 text-bunker-300"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={unresolvedFindings ? faWarning : faCheck} />
|
||||
<span className="text-xs">
|
||||
{totalFindings}{" "}
|
||||
{unresolvedFindings
|
||||
? `Secret ${totalFindings > 1 ? "s" : ""} Detected`
|
||||
: "Leak Resolved"}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
) : status === SecretScanningScanStatus.Failed ? (
|
||||
<span className="text-mineshaft-400">No findings</span>
|
||||
) : (
|
||||
<Badge
|
||||
variant="success"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
No Secrets Detected
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyId();
|
||||
}}
|
||||
>
|
||||
Copy Scan ID
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningScanTable } from "./SecretScanningScanTable";
|
||||
|
||||
type Props = {
|
||||
dataSource: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
export const SecretScanningScanSection = ({ dataSource }: Props) => {
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Scan History</p>
|
||||
</div>
|
||||
<SecretScanningScanTable dataSource={dataSource} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faCheckCircle,
|
||||
faExpand,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import {
|
||||
SecretScanningScanStatus,
|
||||
TSecretScanningDataSource,
|
||||
useListSecretScanningScans
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningScanRow } from "./SecretScanningScanRow";
|
||||
|
||||
enum ScansOrderBy {
|
||||
ResourceName = "resource-name",
|
||||
Findings = "findings",
|
||||
Timestamp = "timestamp",
|
||||
Status = "status"
|
||||
}
|
||||
|
||||
type Props = {
|
||||
dataSource: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
type ScanFilters = {
|
||||
resourceNames: string[];
|
||||
};
|
||||
|
||||
export const SecretScanningScanTable = ({ dataSource }: Props) => {
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const canReadScans = permission.can(
|
||||
ProjectPermissionSecretScanningDataSourceActions.ReadScans,
|
||||
ProjectPermissionSub.SecretScanningDataSources
|
||||
);
|
||||
|
||||
const { data: scans = [], isPending: isScansPending } = useListSecretScanningScans(
|
||||
{ dataSourceId: dataSource.id, type: dataSource.type },
|
||||
{
|
||||
refetchInterval: 30000,
|
||||
enabled: canReadScans
|
||||
}
|
||||
);
|
||||
|
||||
const [filters, setFilters] = useState<ScanFilters>({
|
||||
resourceNames: []
|
||||
});
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<ScansOrderBy>(ScansOrderBy.Timestamp, { initPerPage: 10 });
|
||||
|
||||
const filteredResources = useMemo(
|
||||
() =>
|
||||
scans
|
||||
.filter((scan) => {
|
||||
const { resourceName } = scan;
|
||||
|
||||
if (filters.resourceNames.length && !filters.resourceNames.includes(resourceName))
|
||||
return false;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return resourceName.toLowerCase().includes(searchValue);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [scanOne, scanTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case ScansOrderBy.Findings:
|
||||
if (
|
||||
scanOne.unresolvedFindings === 0 &&
|
||||
scanOne.status === SecretScanningScanStatus.Failed
|
||||
)
|
||||
return 1;
|
||||
if (
|
||||
scanTwo.unresolvedFindings === 0 &&
|
||||
scanTwo.status === SecretScanningScanStatus.Failed
|
||||
)
|
||||
return -1;
|
||||
|
||||
return scanTwo.unresolvedFindings - scanOne.unresolvedFindings;
|
||||
case ScansOrderBy.Timestamp:
|
||||
return new Date(scanTwo.createdAt).getTime() - new Date(scanOne.createdAt).getTime();
|
||||
case ScansOrderBy.ResourceName:
|
||||
default:
|
||||
return scanOne.resourceName
|
||||
.toLowerCase()
|
||||
.localeCompare(scanTwo.resourceName.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[scans, orderDirection, search, orderBy, filters]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredResources.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: ScansOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: ScansOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: ScansOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
const isTableFiltered = Boolean(filters.resourceNames.length);
|
||||
|
||||
const resourceDetails = RESOURCE_DESCRIPTION_HELPER[dataSource.type];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search scans..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter data sources"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuLabel>{resourceDetails.pluralTitle}</DropdownMenuLabel>
|
||||
{scans.length ? (
|
||||
[...new Set(scans.map(({ resourceName }) => resourceName))].map((resourceName) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
resourceNames: prev.resourceNames.includes(resourceName)
|
||||
? prev.resourceNames.filter((a) => a !== resourceName)
|
||||
: [...prev.resourceNames, resourceName]
|
||||
}));
|
||||
}}
|
||||
key={resourceName}
|
||||
icon={
|
||||
filters.resourceNames.includes(resourceName) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
{resourceName}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DropdownMenuItem isDisabled>No Data Sources Configured</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
Timestamp
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(ScansOrderBy.Timestamp)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(ScansOrderBy.Timestamp)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(ScansOrderBy.Timestamp)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/2">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(ScansOrderBy.ResourceName)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(ScansOrderBy.ResourceName)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(ScansOrderBy.ResourceName)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-20">Type</Th>
|
||||
<Th className="w-1/5 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
Findings
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(ScansOrderBy.Findings)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(ScansOrderBy.Findings)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(ScansOrderBy.Findings)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{canReadScans && isScansPending && <TableSkeleton columns={5} innerKey="resource" />}
|
||||
{filteredResources.slice(offset, perPage * page).map((scan) => (
|
||||
<SecretScanningScanRow key={scan.id} scan={scan} />
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!canReadScans && (
|
||||
<EmptyState icon={faBan} title="You do not have permission to view data source scans" />
|
||||
)}
|
||||
{Boolean(filteredResources.length) && (
|
||||
<Pagination
|
||||
count={filteredResources.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{!isScansPending && !filteredResources?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
scans.length
|
||||
? "No scans match search..."
|
||||
: "This data source has no scans associated with it."
|
||||
}
|
||||
icon={scans.length ? faSearch : faExpand}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export * from "./SecretScanningDataSourceSection";
|
||||
export * from "./SecretScanningResourceSection";
|
||||
export * from "./SecretScanningScanSection";
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { SecretScanningDataSourceByIdPage } from "./SecretScanningDataSourceByIdPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
|
||||
)({
|
||||
component: SecretScanningDataSourceByIdPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Data Sources",
|
||||
link: linkOptions({
|
||||
to: "/secret-scanning/$projectId/data-sources",
|
||||
params
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Data Source"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { SecretScanningDataSourcesSection } from "./components";
|
||||
|
||||
export const SecretScanningDataSourcesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Data Sources"
|
||||
description="Manage your Secret Scanning data sources."
|
||||
/>
|
||||
<SecretScanningDataSourcesSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
faBan,
|
||||
faCheck,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faExpand,
|
||||
faInfoCircle,
|
||||
faSearch,
|
||||
faToggleOff,
|
||||
faToggleOn,
|
||||
faTrash,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretScanningScanStatusBadge } from "@app/components/secret-scanning";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import {
|
||||
RESOURCE_DESCRIPTION_HELPER,
|
||||
SECRET_SCANNING_DATA_SOURCE_MAP
|
||||
} from "@app/helpers/secretScanningV2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
SecretScanningFindingStatus,
|
||||
SecretScanningScanStatus,
|
||||
TSecretScanningDataSourceWithDetails
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
dataSource: TSecretScanningDataSourceWithDetails;
|
||||
onDelete: (dataSource: TSecretScanningDataSourceWithDetails) => void;
|
||||
onTriggerScan: (dataSource: TSecretScanningDataSourceWithDetails) => void;
|
||||
onToggleEnableAutoScan: (dataSource: TSecretScanningDataSourceWithDetails) => void;
|
||||
onEdit: (dataSource: TSecretScanningDataSourceWithDetails) => void;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceRow = ({
|
||||
dataSource,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onTriggerScan,
|
||||
onToggleEnableAutoScan
|
||||
}: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
isAutoScanEnabled,
|
||||
projectId,
|
||||
type,
|
||||
unresolvedFindings,
|
||||
lastScannedAt,
|
||||
lastScanStatus,
|
||||
lastScanStatusMessage
|
||||
} = dataSource;
|
||||
|
||||
const sourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||
|
||||
const handleCopyId = useCallback(() => {
|
||||
setIsIdCopied.on();
|
||||
navigator.clipboard.writeText(id);
|
||||
|
||||
createNotification({
|
||||
text: "Data Source ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isIdCopied]);
|
||||
|
||||
const autoScanDescription = RESOURCE_DESCRIPTION_HELPER[type];
|
||||
|
||||
return (
|
||||
<Tr
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: ROUTE_PATHS.SecretScanning.DataSourceByIdPage.path,
|
||||
params: {
|
||||
dataSourceId: id,
|
||||
type,
|
||||
projectId
|
||||
}
|
||||
})
|
||||
}
|
||||
className={twMerge(
|
||||
"group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700",
|
||||
lastScanStatus === SecretScanningScanStatus.Failed && "bg-red/5 hover:bg-red/10"
|
||||
)}
|
||||
key={`data-source-${id}`}
|
||||
>
|
||||
<Td className="!min-w-[8rem] max-w-0">
|
||||
<div className="flex w-full items-center">
|
||||
<img
|
||||
alt={`${sourceDetails.name} Data Source`}
|
||||
src={`/images/integrations/${sourceDetails.image}`}
|
||||
className="w-5"
|
||||
/>
|
||||
<p className="ml-2 truncate">{sourceDetails.name}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="!min-w-[8rem] max-w-0">
|
||||
<div className="flex w-full items-center">
|
||||
<p className="truncate">{name}</p>
|
||||
{description && (
|
||||
<Tooltip content={description}>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="ml-1 text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{lastScanStatus?.match(/queued|scanning/) ? (
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="flex h-5 w-min animate-pulse items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
<span>Scanning For Leaks</span>
|
||||
</Badge>
|
||||
) : // eslint-disable-next-line no-nested-ternary
|
||||
lastScannedAt ? (
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
unresolvedFindings ? (
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="flex h-5 w-min cursor-pointer items-center gap-1.5 whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: "/secret-scanning/$projectId/findings",
|
||||
params: {
|
||||
projectId
|
||||
},
|
||||
search: {
|
||||
search: name,
|
||||
status: SecretScanningFindingStatus.Unresolved
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} />
|
||||
<span>
|
||||
{unresolvedFindings} Secret{unresolvedFindings > 1 ? "s" : ""} Detected
|
||||
</span>
|
||||
</Badge>
|
||||
) : lastScanStatus === SecretScanningScanStatus.Failed ? (
|
||||
<span className="text-mineshaft-400">No findings</span>
|
||||
) : (
|
||||
<Badge
|
||||
variant="success"
|
||||
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
No Secrets Detected
|
||||
</Badge>
|
||||
)
|
||||
) : (
|
||||
<span className="text-mineshaft-400">No findings</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex w-full items-center gap-1">
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{lastScanStatus?.match(/queued|scanning|failed/) ? (
|
||||
<SecretScanningScanStatusBadge
|
||||
status={lastScanStatus}
|
||||
statusMessage={lastScanStatusMessage}
|
||||
scannedAt={lastScannedAt}
|
||||
/>
|
||||
) : lastScannedAt ? (
|
||||
<span>{formatDistance(new Date(lastScannedAt), new Date(), { addSuffix: true })}</span>
|
||||
) : (
|
||||
<span className="text-mineshaft-400">No scans</span>
|
||||
)}
|
||||
{!isAutoScanEnabled && (
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content={`Auto-Scan is disabled. Scans will not be automatically triggered when a ${autoScanDescription.verb} occurs to ${autoScanDescription.pluralNoun} associated with this data source`}
|
||||
>
|
||||
<div className="ml-auto">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
<span>Auto-Scan Disabled</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyId();
|
||||
}}
|
||||
>
|
||||
Copy Data Source ID
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.TriggerScans}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faExpand} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTriggerScan(dataSource);
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip
|
||||
position="left"
|
||||
sideOffset={42}
|
||||
content={`Manually trigger a scan for this ${sourceDetails.name} data source.`}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-between gap-1">
|
||||
<span> Trigger Scan</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-bunker-300"
|
||||
size="sm"
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.Edit}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={isAutoScanEnabled ? faToggleOff : faToggleOn} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleEnableAutoScan(dataSource);
|
||||
}}
|
||||
>
|
||||
{isAutoScanEnabled ? "Disable" : "Enable"} Auto-Scan
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.Edit}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(dataSource);
|
||||
}}
|
||||
>
|
||||
Edit Data Source
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.Delete}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(dataSource);
|
||||
}}
|
||||
>
|
||||
Delete Data Source
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { CreateSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
||||
import { Button, Spinner } from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSecretScanningDataSourceActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useListSecretScanningDataSources } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningDataSourcesTable } from "./SecretScanningDataSourcesTable";
|
||||
|
||||
export const SecretScanningDataSourcesSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addDataSource"] as const);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: dataSources = [], isPending: isDataSourcesPending } =
|
||||
useListSecretScanningDataSources(currentWorkspace.id, {
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
if (isDataSourcesPending)
|
||||
return (
|
||||
<div className="flex h-[60vh] flex-col items-center justify-center gap-2">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Data Sources</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Use App Connections to scan for secret leaks from third-party services.
|
||||
</p>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretScanningDataSourceActions.Create}
|
||||
a={ProjectPermissionSub.SecretScanningDataSources}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addDataSource")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Data Source
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<SecretScanningDataSourcesTable dataSources={dataSources} />
|
||||
</div>
|
||||
<CreateSecretScanningDataSourceModal
|
||||
isOpen={popUp.addDataSource.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addDataSource", isOpen)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCheckCircle,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faPuzzlePiece,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { EditSecretScanningDataSourceModal } from "@app/components/secret-scanning";
|
||||
import { DeleteSecretScanningDataSourceModal } from "@app/components/secret-scanning/DeleteSecretScanningDataSourceModal";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningScanStatus,
|
||||
TSecretScanningDataSource,
|
||||
TSecretScanningDataSourceWithDetails,
|
||||
useTriggerSecretScanningDataSource,
|
||||
useUpdateSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningDataSourceRow } from "./SecretScanningDataSourceRow";
|
||||
|
||||
// import { getSecretSyncDestinationColValues } from "./helpers";
|
||||
|
||||
enum DataSourcesOrderBy {
|
||||
Findings = "findings",
|
||||
Name = "name",
|
||||
LastScan = "last-scan"
|
||||
}
|
||||
|
||||
type DataSourceFilters = {
|
||||
types: SecretScanningDataSource[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
dataSources: TSecretScanningDataSourceWithDetails[];
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourcesTable = ({ dataSources }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"deleteDataSource",
|
||||
"editDataSource",
|
||||
"triggerDataSourceScan"
|
||||
] as const);
|
||||
const triggerDataSourceScan = useTriggerSecretScanningDataSource();
|
||||
const updateDataSource = useUpdateSecretScanningDataSource();
|
||||
|
||||
const [filters, setFilters] = useState<DataSourceFilters>({
|
||||
types: []
|
||||
});
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<DataSourcesOrderBy>(DataSourcesOrderBy.Name, { initPerPage: 20 });
|
||||
|
||||
const filteredDataSources = useMemo(
|
||||
() =>
|
||||
dataSources
|
||||
.filter((dataSource) => {
|
||||
const { type, name, connection } = dataSource;
|
||||
|
||||
if (filters.types.length && !filters.types.includes(type)) return false;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
SECRET_SCANNING_DATA_SOURCE_MAP[type].name.toLowerCase().includes(searchValue) ||
|
||||
name.toLowerCase().includes(searchValue) ||
|
||||
connection.name.toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [dataSourceOne, dataSourceTwo] =
|
||||
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case DataSourcesOrderBy.LastScan:
|
||||
if (dataSourceOne.lastScannedAt === null && dataSourceTwo.lastScannedAt === null)
|
||||
return 0;
|
||||
if (dataSourceOne.lastScannedAt === null) return 1;
|
||||
if (dataSourceTwo.lastScannedAt === null) return -1;
|
||||
|
||||
return (
|
||||
new Date(dataSourceTwo.lastScannedAt).getTime() -
|
||||
new Date(dataSourceOne.lastScannedAt).getTime()
|
||||
);
|
||||
case DataSourcesOrderBy.Findings:
|
||||
if (
|
||||
dataSourceOne.unresolvedFindings === null &&
|
||||
dataSourceTwo.unresolvedFindings === null
|
||||
)
|
||||
return 0;
|
||||
if (dataSourceOne.unresolvedFindings === null) return 1;
|
||||
if (dataSourceTwo.unresolvedFindings === null) return -1;
|
||||
|
||||
if (
|
||||
dataSourceOne.unresolvedFindings === 0 &&
|
||||
dataSourceOne.lastScanStatus === SecretScanningScanStatus.Failed
|
||||
)
|
||||
return 1;
|
||||
if (
|
||||
dataSourceTwo.unresolvedFindings === 0 &&
|
||||
dataSourceTwo.lastScanStatus === SecretScanningScanStatus.Failed
|
||||
)
|
||||
return -1;
|
||||
|
||||
return dataSourceTwo.unresolvedFindings - dataSourceOne.unresolvedFindings;
|
||||
case DataSourcesOrderBy.Name:
|
||||
default:
|
||||
return dataSourceOne.name
|
||||
.toLowerCase()
|
||||
.localeCompare(dataSourceTwo.name.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[dataSources, orderDirection, search, orderBy, filters]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredDataSources.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: DataSourcesOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: DataSourcesOrderBy) =>
|
||||
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: DataSourcesOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
const isTableFiltered = Boolean(filters.types.length);
|
||||
|
||||
const handleDelete = (dataSource: TSecretScanningDataSource) =>
|
||||
handlePopUpOpen("deleteDataSource", dataSource);
|
||||
|
||||
const handleEdit = (dataSource: TSecretScanningDataSource) =>
|
||||
handlePopUpOpen("editDataSource", dataSource);
|
||||
|
||||
const handleToggleEnableAutoScan = async (dataSource: TSecretScanningDataSource) => {
|
||||
const destinationName = SECRET_SCANNING_DATA_SOURCE_MAP[dataSource.type].name;
|
||||
|
||||
const isAutoScanEnabled = !dataSource.isAutoScanEnabled;
|
||||
|
||||
try {
|
||||
await updateDataSource.mutateAsync({
|
||||
dataSourceId: dataSource.id,
|
||||
type: dataSource.type,
|
||||
isAutoScanEnabled,
|
||||
projectId: dataSource.projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isAutoScanEnabled ? "enabled" : "disabled"} auto-scan for ${destinationName} Data Source`,
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to ${isAutoScanEnabled ? "enable" : "disable"} auto-scan for ${destinationName} Data Source`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTriggerScan = async (dataSource: TSecretScanningDataSource) => {
|
||||
try {
|
||||
await triggerDataSourceScan.mutateAsync({
|
||||
dataSourceId: dataSource.id,
|
||||
type: dataSource.type,
|
||||
projectId: dataSource.projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully triggered scan",
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to trigger scan",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search data sources..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter data sources"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuLabel>Platform</DropdownMenuLabel>
|
||||
{dataSources.length ? (
|
||||
[...new Set(dataSources.map(({ type }) => type))].map((type) => {
|
||||
const { name, image } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
types: prev.types.includes(type)
|
||||
? prev.types.filter((a) => a !== type)
|
||||
: [...prev.types, type]
|
||||
}));
|
||||
}}
|
||||
key={type}
|
||||
icon={
|
||||
filters.types.includes(type) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${name} integration`}
|
||||
src={`/images/integrations/${image}`}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DropdownMenuItem isDisabled>No Data Sources Configured</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-60">Platform</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(DataSourcesOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(DataSourcesOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(DataSourcesOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Findings
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(DataSourcesOrderBy.Findings)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(DataSourcesOrderBy.Findings)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(DataSourcesOrderBy.Findings)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Last Scan
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(DataSourcesOrderBy.LastScan)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(DataSourcesOrderBy.LastScan)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(DataSourcesOrderBy.LastScan)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{filteredDataSources.slice(offset, perPage * page).map((dataSource) => (
|
||||
<SecretScanningDataSourceRow
|
||||
key={dataSource.id}
|
||||
dataSource={dataSource}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
onTriggerScan={handleTriggerScan}
|
||||
onToggleEnableAutoScan={handleToggleEnableAutoScan}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredDataSources.length) && (
|
||||
<Pagination
|
||||
count={filteredDataSources.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{!filteredDataSources?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
dataSources.length
|
||||
? "No data sources match search..."
|
||||
: "This project has no data sources configured"
|
||||
}
|
||||
icon={dataSources.length ? faSearch : faPuzzlePiece}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<DeleteSecretScanningDataSourceModal
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteDataSource", isOpen)}
|
||||
isOpen={popUp.deleteDataSource.isOpen}
|
||||
dataSource={popUp.deleteDataSource.data}
|
||||
/>
|
||||
<EditSecretScanningDataSourceModal
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editDataSource", isOpen)}
|
||||
isOpen={popUp.editDataSource.isOpen}
|
||||
dataSource={popUp.editDataSource.data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretScanningDataSourcesSection } from "./SecretScanningDataSourcesSection";
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { SecretScanningDataSourcesPage } from "./SecretScanningDataSourcesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources/"
|
||||
)({
|
||||
component: SecretScanningDataSourcesPage
|
||||
// beforeLoad: ({ context }) => {
|
||||
// return {
|
||||
// breadcrumbs: [
|
||||
// ...context.breadcrumbs,
|
||||
// {
|
||||
// label: "Data Sources"
|
||||
// }
|
||||
// ]
|
||||
// };
|
||||
// }
|
||||
});
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { SecretScanningFindingsSection } from "./components";
|
||||
|
||||
export const SecretScanningFindingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Secret Scanning" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Findings" description="View Secret Leaks across your project." />
|
||||
<SecretScanningFindingsSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy, faEllipsisV } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
GenericFieldLabel,
|
||||
IconButton,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
SECRET_SCANNING_DATA_SOURCE_MAP,
|
||||
SECRET_SCANNING_FINDING_STATUS_ICON_MAP
|
||||
} from "@app/helpers/secretScanningV2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
SecretScanningFindingStatus,
|
||||
TSecretScanningFinding
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
finding: TSecretScanningFinding;
|
||||
onUpdate: (finding: TSecretScanningFinding) => void;
|
||||
};
|
||||
|
||||
export const SecretScanningFindingRow = ({ finding, onUpdate }: Props) => {
|
||||
const {
|
||||
resourceName,
|
||||
id,
|
||||
dataSourceType,
|
||||
createdAt,
|
||||
dataSourceName,
|
||||
rule,
|
||||
status,
|
||||
details,
|
||||
remarks
|
||||
} = finding;
|
||||
|
||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||
|
||||
const handleCopyId = useCallback(
|
||||
(idToCopy: string) => {
|
||||
setIsIdCopied.on();
|
||||
navigator.clipboard.writeText(idToCopy);
|
||||
|
||||
createNotification({
|
||||
text: "Resource ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
},
|
||||
[isIdCopied]
|
||||
);
|
||||
|
||||
const sourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[dataSourceType];
|
||||
|
||||
const [isExpanded, setIsExpanded] = useToggle(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
onClick={setIsExpanded.toggle}
|
||||
className={twMerge(
|
||||
"group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
)}
|
||||
key={`resource-${id}`}
|
||||
>
|
||||
<Td className="!min-w-[4rem] max-w-0">
|
||||
<div className="flex w-full items-center">
|
||||
<img
|
||||
alt={`${sourceDetails.name} Data Source`}
|
||||
src={`/images/integrations/${sourceDetails.image}`}
|
||||
className="w-5"
|
||||
/>
|
||||
<p className="ml-2 truncate">{sourceDetails.name}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||
<p>{format(createdAt, "MMM dd yyyy")}</p>
|
||||
<p className="text-mineshaft-300">{format(createdAt, "h:mm aa")}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="!min-w-[8rem] max-w-0">
|
||||
<div className="w-full items-center">
|
||||
<p className="truncate">{resourceName}</p>
|
||||
<p className="truncate text-xs text-mineshaft-400">{dataSourceName}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="whitespace-nowrap">{rule}</Td>
|
||||
<Td className="whitespace-nowrap">
|
||||
<Tooltip position="left" content={remarks}>
|
||||
<div className="w-min">
|
||||
<Badge
|
||||
variant={status === SecretScanningFindingStatus.Resolved ? "success" : "primary"}
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap",
|
||||
(status === SecretScanningFindingStatus.FalsePositive ||
|
||||
status === SecretScanningFindingStatus.Ignore) &&
|
||||
"bg-mineshaft-400/50 text-bunker-300"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].icon} />
|
||||
<span className="capitalize">{status.replace("-", " ")}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyId(id);
|
||||
}}
|
||||
>
|
||||
Copy Finding ID
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td colSpan={6} className="!border-none p-0">
|
||||
<div
|
||||
className={`w-full overflow-hidden bg-mineshaft-900/75 transition-all duration-500 ${
|
||||
isExpanded ? "max-h-[50rem] opacity-100" : "max-h-0"
|
||||
}`}
|
||||
>
|
||||
<div className="grid gap-4 p-4 2xl:grid-cols-6">
|
||||
<GenericFieldLabel truncate className="col-span-full" label="Description">
|
||||
{details.description}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel truncate label="Date">
|
||||
{format(details.date, "MMM dd yyyy h:mm aa")}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel truncate label="Author">
|
||||
{details.author}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-4" truncate label="Email">
|
||||
{details.email}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel truncate className="col-span-full" label="File">
|
||||
{details.file}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel truncate className="col-span-full" label="Commit">
|
||||
{details.commit}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-full" label="Commit Message">
|
||||
{details.message}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Start Line">{details.startLine}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="End Line">{details.endLine}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Start Column">{details.startColumn}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="End Column">{details.endColumn}</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-full" label="Link">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cursor-pointer text-mineshaft-300 underline underline-offset-2 hover:text-mineshaft-100"
|
||||
href={details.link}
|
||||
>
|
||||
{details.link}
|
||||
</a>
|
||||
</GenericFieldLabel>
|
||||
|
||||
<Button
|
||||
onClick={() => onUpdate(finding)}
|
||||
colorSchema="secondary"
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
className={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].className}
|
||||
icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].icon}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Update Status
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Spinner } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useListSecretScanningFindings } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningFindingsTable } from "./SecretScanningFindingsTable";
|
||||
|
||||
export const SecretScanningFindingsSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: findings = [], isPending: isFindingsPending } = useListSecretScanningFindings(
|
||||
currentWorkspace.id,
|
||||
{
|
||||
refetchInterval: 30000
|
||||
}
|
||||
);
|
||||
|
||||
if (isFindingsPending)
|
||||
return (
|
||||
<div className="flex h-[60vh] flex-col items-center justify-center gap-2">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Findings</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Secret leaks discovered across your data sources
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<SecretScanningFindingsTable findings={findings} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user