mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 02:49:06 +00:00
feature: secret scanning architecture and github data source (wip)
This commit is contained in:
+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"
|
||||
// }
|
||||
// ]
|
||||
// };
|
||||
// }
|
||||
});
|
||||
Reference in New Issue
Block a user