feature: secret scanning architecture and github data source (wip)

This commit is contained in:
Scott Wilson
2025-05-28 22:21:03 -07:00
parent 931ee1e8da
commit e6c97510ca
231 changed files with 11443 additions and 1048 deletions
@@ -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>
</>
);
};
@@ -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>
</>
);
};
@@ -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>
);
};
@@ -0,0 +1,372 @@
import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
SECRET_SCANNING_DATA_SOURCE_MAP,
SECRET_SCANNING_FINDING_STATUS_ICON_MAP
} from "@app/helpers/secretScanningV2";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import {
SecretScanningDataSource,
SecretScanningFindingStatus,
TSecretScanningFinding
} from "@app/hooks/api/secretScanningV2";
import { SecretScanningFindingRow } from "./SecretScanningFindingRow";
import { SecretScanningUpdateFindingModal } from "./SecretScanningUpdateFindingModal";
enum FindingsOrderBy {
ResourceName = "resource-name",
Status = "status",
Rule = "rule",
Timestamp = "timestamp"
}
type DataSourceFilters = {
dataSourceTypes: SecretScanningDataSource[];
status: SecretScanningFindingStatus[];
};
type Props = {
findings: TSecretScanningFinding[];
};
const getStatusOrderValue = (status: SecretScanningFindingStatus) => {
switch (status) {
case SecretScanningFindingStatus.Unresolved:
return 0;
case SecretScanningFindingStatus.Ignore:
return 1;
case SecretScanningFindingStatus.FalsePositive:
return 2;
default:
return 3;
}
};
export const SecretScanningFindingsTable = ({ findings }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["updateFinding"] as const);
const { search: initSearch, status: initStatus } = useSearch({
from: ROUTE_PATHS.SecretScanning.FindingsPage.id
});
const [filters, setFilters] = useState<DataSourceFilters>({
dataSourceTypes: [],
status: initStatus ? [initStatus] : []
});
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy
} = usePagination<FindingsOrderBy>(FindingsOrderBy.Timestamp, { initPerPage: 20, initSearch });
const filteredFindings = useMemo(
() =>
findings
.filter((finding) => {
const { rule, resourceName, dataSourceType, dataSourceName, status, scanId } = finding;
if (filters.dataSourceTypes.length && !filters.dataSourceTypes.includes(dataSourceType))
return false;
if (filters.status.length && !filters.status.includes(status)) {
return false;
}
if (search.includes("scanId")) {
return search.split(":")[1] === scanId;
}
const searchValue = search.trim().toLowerCase();
return (
SECRET_SCANNING_DATA_SOURCE_MAP[dataSourceType].name
.toLowerCase()
.includes(searchValue) ||
resourceName.toLowerCase().includes(searchValue) ||
dataSourceName.toLowerCase().includes(searchValue) ||
rule.toLowerCase().includes(searchValue)
);
})
.sort((a, b) => {
const [findingOne, findingTwo] =
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
switch (orderBy) {
case FindingsOrderBy.Timestamp:
return (
new Date(findingTwo.createdAt).getTime() - new Date(findingOne.createdAt).getTime()
);
case FindingsOrderBy.Rule:
return findingOne.rule.toLowerCase().localeCompare(findingTwo.rule.toLowerCase());
case FindingsOrderBy.Status:
return (
getStatusOrderValue(findingOne.status) - getStatusOrderValue(findingTwo.status)
);
case FindingsOrderBy.ResourceName:
default:
return findingOne.resourceName
.toLowerCase()
.localeCompare(findingTwo.resourceName.toLowerCase());
}
}),
[findings, orderDirection, search, orderBy, filters]
);
useResetPageHelper({
totalCount: filteredFindings.length,
offset,
setPage
});
const handleSort = (column: FindingsOrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: FindingsOrderBy) =>
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: FindingsOrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.dataSourceTypes.length || filters.status.length);
return (
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search findings..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter findings"
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>Status</DropdownMenuLabel>
{Object.values(SecretScanningFindingStatus).map((status) => (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
status: prev.status.includes(status)
? prev.status.filter((s) => s !== status)
: [...prev.status, status]
}));
}}
key={status}
icon={
filters.status.includes(status) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<FontAwesomeIcon
icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].icon}
className={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[status].className}
/>
<span className="capitalize">{status.replace("-", " ")}</span>
</div>
</DropdownMenuItem>
))}
<DropdownMenuLabel>Platform</DropdownMenuLabel>
{findings.length ? (
[...new Set(findings.map(({ dataSourceType: type }) => type))].map((type) => {
const { name, image } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
dataSourceTypes: prev.dataSourceTypes.includes(type)
? prev.dataSourceTypes.filter((a) => a !== type)
: [...prev.dataSourceTypes, type]
}));
}}
key={type}
icon={
filters.dataSourceTypes.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 Source Findings</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="min-w-[10rem]">Platform</Th>
<Th className="w-1/4">
<div className="flex items-center">
Timestamp
<IconButton
variant="plain"
className={getClassName(FindingsOrderBy.Timestamp)}
ariaLabel="sort"
onClick={() => handleSort(FindingsOrderBy.Timestamp)}
>
<FontAwesomeIcon icon={getColSortIcon(FindingsOrderBy.Timestamp)} />
</IconButton>
</div>
</Th>
<Th className="w-1/3 whitespace-nowrap">
<div className="flex items-center">
Resource Name
<IconButton
variant="plain"
className={getClassName(FindingsOrderBy.ResourceName)}
ariaLabel="sort"
onClick={() => handleSort(FindingsOrderBy.ResourceName)}
>
<FontAwesomeIcon icon={getColSortIcon(FindingsOrderBy.ResourceName)} />
</IconButton>
</div>
</Th>
<Th className="w-1/4">
<div className="flex items-center">
Rule
<IconButton
variant="plain"
className={getClassName(FindingsOrderBy.Rule)}
ariaLabel="sort"
onClick={() => handleSort(FindingsOrderBy.Rule)}
>
<FontAwesomeIcon icon={getColSortIcon(FindingsOrderBy.Rule)} />
</IconButton>
</div>
</Th>
<Th className="w-1/4">
<div className="flex items-center">
Status
<IconButton
variant="plain"
className={getClassName(FindingsOrderBy.Status)}
ariaLabel="sort"
onClick={() => handleSort(FindingsOrderBy.Status)}
>
<FontAwesomeIcon icon={getColSortIcon(FindingsOrderBy.Status)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{filteredFindings.slice(offset, perPage * page).map((finding) => (
<SecretScanningFindingRow
key={finding.id}
finding={finding}
onUpdate={() => handlePopUpOpen("updateFinding", finding)}
/>
))}
</TBody>
</Table>
{Boolean(filteredFindings.length) && (
<Pagination
count={filteredFindings.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
/>
)}
{!filteredFindings?.length && (
<EmptyState
title={
findings.length
? "No findings match search..."
: "This project has not uncovered any secret leaks"
}
icon={faSearch}
/>
)}
</TableContainer>
<SecretScanningUpdateFindingModal
isOpen={popUp.updateFinding.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("updateFinding", isOpen)}
finding={popUp.updateFinding.data}
/>
</div>
);
};
@@ -0,0 +1,141 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import { SECRET_SCANNING_FINDING_STATUS_ICON_MAP } from "@app/helpers/secretScanningV2";
import {
SecretScanningFindingStatus,
TSecretScanningFinding,
useUpdateSecretScanningFinding
} from "@app/hooks/api/secretScanningV2";
type Props = {
finding?: TSecretScanningFinding;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
const FormSchema = z.object({
remarks: z.string().optional(),
status: z.nativeEnum(SecretScanningFindingStatus)
});
type FormType = z.infer<typeof FormSchema>;
type ContentProps = {
finding: TSecretScanningFinding;
onComplete: () => void;
};
const Content = ({ finding, onComplete }: ContentProps) => {
const updateFinding = useUpdateSecretScanningFinding();
const { handleSubmit, control } = useForm<FormType>({
resolver: zodResolver(FormSchema),
defaultValues: {
status: finding.status,
remarks: finding.remarks ?? ""
}
});
const onSubmit = async (data: FormType) => {
try {
await updateFinding.mutateAsync({
...data,
findingId: finding.id,
projectId: finding.projectId
});
createNotification({
type: "success",
text: "Finding status successfully updated"
});
onComplete();
} catch {
createNotification({
type: "error",
text: "Failed to update Finding status"
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="status"
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl label="Status" isError={Boolean(error)} errorText={error?.message}>
<Select
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
icon={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].icon}
iconClassName={SECRET_SCANNING_FINDING_STATUS_ICON_MAP[value].className}
>
{Object.values(SecretScanningFindingStatus).map((status) => {
return (
<SelectItem className="capitalize" value={status} key={status}>
{status.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</FormControl>
);
}}
/>
<Controller
control={control}
name="remarks"
render={({ field, fieldState: { error } }) => {
return (
<FormControl label="Remarks" isError={Boolean(error)} errorText={error?.message}>
<TextArea className="h-40 !resize-none" {...field} />
</FormControl>
);
}}
/>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button
type="submit"
isLoading={updateFinding.isPending}
isDisabled={updateFinding.isPending}
colorSchema="secondary"
>
Update Status
</Button>
<ModalClose asChild>
<Button colorSchema="secondary">Cancel</Button>
</ModalClose>
</div>
</form>
);
};
export const SecretScanningUpdateFindingModal = ({ finding, isOpen, onOpenChange }: Props) => {
if (!finding) return null;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Resolve Finding" subTitle="Mark this finding as resolved">
<Content finding={finding} onComplete={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export * from "./SecretScanningFindingsSection";
@@ -0,0 +1,29 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { SecretScanningFindingStatus } from "@app/hooks/api/secretScanningV2";
import { SecretScanningFindingsPage } from "./SecretScanningFindingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/findings"
)({
validateSearch: zodValidator(
z.object({
search: z.string().optional(),
status: z.nativeEnum(SecretScanningFindingStatus).optional().catch(undefined)
})
),
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Findings"
}
]
};
},
component: SecretScanningFindingsPage
});