import { useCallback } from "react"; import { faCheck, faCopy, faEllipsisV, faExpand, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { formatDistance } from "date-fns"; import { AlertTriangleIcon, BanIcon, ScanTextIcon } from "lucide-react"; 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, IconButton, Td, Tooltip, Tr } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { ProjectPermissionSecretScanningDataSourceActions, ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2"; import { useToggle } from "@app/hooks"; import { SecretScanningDataSource, SecretScanningFindingStatus, SecretScanningScanStatus, TSecretScanningDataSource, TSecretScanningResourceWithDetails, useTriggerSecretScanningDataSource } from "@app/hooks/api/secretScanningV2"; import { GitLabDataSourceScope } from "@app/hooks/api/secretScanningV2/types/gitlab-data-source"; type Props = { resource: TSecretScanningResourceWithDetails; dataSource: TSecretScanningDataSource; }; export const SecretScanningResourceRow = ({ resource, dataSource }: Props) => { const { id, name, lastScannedAt, lastScanStatus, unresolvedFindings, lastScanStatusMessage } = resource; let isActive: boolean; switch (dataSource.type) { case SecretScanningDataSource.Bitbucket: case SecretScanningDataSource.GitHub: isActive = dataSource.config.includeRepos.includes("*") || dataSource.config.includeRepos.includes(name); break; case SecretScanningDataSource.GitLab: { if (dataSource.config.scope === GitLabDataSourceScope.Project) { isActive = true; // always active } else { isActive = dataSource.config.includeProjects.includes("*") || dataSource.config.includeProjects.includes(name); } break; } default: throw new Error("Unhandled Data Source Type: Active Status"); } // scott: will need to be differentiated by type once other data sources are available const triggerDataSourceScan = useTriggerSecretScanningDataSource(); const navigate = useNavigate(); const handleTriggerScan = async () => { await triggerDataSourceScan.mutateAsync({ dataSourceId: dataSource.id, type: dataSource.type, projectId: dataSource.projectId, resourceId: id }); createNotification({ text: `Successfully triggered scan for ${name}`, type: "success" }); }; 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 (

{name}

{/* eslint-disable-next-line no-nested-ternary */} {lastScanStatus?.match(/queued|scanning/) ? ( Scanning For Leaks ) : // eslint-disable-next-line no-nested-ternary lastScannedAt ? ( // eslint-disable-next-line no-nested-ternary unresolvedFindings ? ( ) : lastScanStatus === SecretScanningScanStatus.Failed ? ( No findings ) : ( No Secrets Detected ) ) : ( No findings )} {/* eslint-disable-next-line no-nested-ternary */} {lastScanStatus?.match(/queued|scanning|failed/) ? ( ) : lastScannedAt ? ( formatDistance(new Date(lastScannedAt), new Date(), { addSuffix: true }) ) : ( No scans )}
{!isActive && ( Inactive )} } onClick={(e) => { e.stopPropagation(); handleCopyId(id); }} > Copy Resource ID {(isAllowed) => ( } onClick={(e) => { e.stopPropagation(); handleTriggerScan(); }} >
Trigger Scan
)}
); };