From ab3ee775bbe16db7f38506acda8836680af9e247 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 14 Jan 2025 00:53:40 +0100 Subject: [PATCH] feat(radar): export radar data --- backend/src/server/plugins/secret-scanner.ts | 7 +- frontend/src/lib/fn/csv.ts | 48 ++++ .../SecretScanningPage/SecretScanningPage.tsx | 34 ++- .../components/ExportResultsModal.tsx | 244 ++++++++++++++++++ .../components/SecretScanningLogsTable.tsx | 14 +- .../organization/SecretScanningPage/route.tsx | 34 +-- 6 files changed, 349 insertions(+), 32 deletions(-) create mode 100644 frontend/src/lib/fn/csv.ts create mode 100644 frontend/src/pages/organization/SecretScanningPage/components/ExportResultsModal.tsx diff --git a/backend/src/server/plugins/secret-scanner.ts b/backend/src/server/plugins/secret-scanner.ts index d9b5801b9..96ad5c72c 100644 --- a/backend/src/server/plugins/secret-scanner.ts +++ b/backend/src/server/plugins/secret-scanner.ts @@ -1,3 +1,4 @@ +import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types"; import { PushEvent } from "@octokit/webhooks-types"; import { Probot } from "probot"; import SmeeClient from "smee-client"; @@ -54,14 +55,14 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) => rateLimit: writeLimit }, handler: async (req, res) => { - const eventName = req.headers["x-github-event"]; + const eventName = req.headers["x-github-event"] as EmitterWebhookEventName; const signatureSHA256 = req.headers["x-hub-signature-256"] as string; const id = req.headers["x-github-delivery"] as string; + await probot.webhooks.verifyAndReceive({ id, - // @ts-expect-error type name: eventName, - payload: req.body as string, + payload: JSON.stringify(req.body), signature: signatureSHA256 }); void res.send("ok"); diff --git a/frontend/src/lib/fn/csv.ts b/frontend/src/lib/fn/csv.ts new file mode 100644 index 000000000..d10fc0660 --- /dev/null +++ b/frontend/src/lib/fn/csv.ts @@ -0,0 +1,48 @@ +/** + * Converts a JSON array of objects to CSV format + * @param {Array} jsonData - Array of objects to convert + * @param {string} filename - Name of the file to download (without extension) + */ +export const convertJsonToCsv = (jsonData: Record[]) => { + // Get headers from the first object + + if (jsonData.length === 0) { + return new Blob([""], { type: "text/csv;charset=utf-8;" }); + } + + const headers = Object.keys(jsonData[0]); + + const csvRows = [ + headers.join(","), + // Add all data rows + ...jsonData.map((row) => { + return headers + .map((header) => { + // Handle special cases in the data + let cell = row[header]; + + // Convert null/undefined to empty string + if (cell === null || cell === undefined) { + return ""; + } + + // Handle normal values + if (typeof cell === "number" || typeof cell === "boolean") { + return cell; + } + + // Convert objects/arrays to JSON string + if (typeof cell === "object") { + cell = JSON.stringify(cell); + } + + // Escape quotes and wrap in quotes + cell = cell.toString().replace(/"/g, '""'); + return `"${cell}"`; + }) + .join(","); + }) + ].join("\n"); + + return new Blob([csvRows], { type: "text/csv;charset=utf-8;" }); +}; diff --git a/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx b/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx index b5fae842c..44546edbe 100644 --- a/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx +++ b/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx @@ -12,23 +12,29 @@ import { useServerConfig } from "@app/context"; import { withPermission } from "@app/hoc"; +import { usePopUp } from "@app/hooks"; import { useCreateNewInstallationSession, useGetSecretScanningInstallationStatus, + useGetSecretScanningRisks, useLinkGitAppInstallationWithOrg } from "@app/hooks/api/secretScanning"; +import { ExportSecretScansModal } from "./components/ExportResultsModal"; import { SecretScanningLogsTable } from "./components"; export const SecretScanningPage = withPermission( () => { const queryParams = useSearch({ from: ROUTE_PATHS.Organization.SecretScanning.id - }); + }) as Record; + const { config } = useServerConfig(); const { currentOrg } = useOrganization(); const organizationId = currentOrg.id; + const { isPending, data: gitRisks } = useGetSecretScanningRisks(organizationId); + const { mutateAsync: linkGitAppInstallationWithOrganization } = useLinkGitAppInstallationWithOrg(); const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession(); @@ -37,13 +43,15 @@ export const SecretScanningPage = withPermission( 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: queryParams.installation_id as string, - sessionId: queryParams.state as string + installationId: String(queryParams.installation_id), + sessionId: String(queryParams.state) }); if (isLinked) { window.location.reload(); @@ -60,7 +68,7 @@ export const SecretScanningPage = withPermission( const generateNewIntegrationSession = async () => { const session = await createNewIntegrationSession({ organizationId }); - window.location.href = `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`; + window.location.href = `https://github.com/apps/infisical-radar-dev-2/installations/new?state=${session.sessionId}`; }; return ( @@ -132,9 +140,25 @@ export const SecretScanningPage = withPermission( )} - +
+
+ +
+ +
+ ); }, diff --git a/frontend/src/pages/organization/SecretScanningPage/components/ExportResultsModal.tsx b/frontend/src/pages/organization/SecretScanningPage/components/ExportResultsModal.tsx new file mode 100644 index 000000000..da395e688 --- /dev/null +++ b/frontend/src/pages/organization/SecretScanningPage/components/ExportResultsModal.tsx @@ -0,0 +1,244 @@ +import { useMemo } from "react"; +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 { TSecretScanningGitRisks } 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; + gitRisks: TSecretScanningGitRisks[]; +}; + +enum ExportFormat { + Json = "json", + Csv = "csv" +} + +enum ExportStatus { + All = "all", + Unresolved = "unresolved", + Resolved = "resolved" +} + +const formSchema = z.object({ + githubOrganization: z.string().trim(), + githubRepository: z.string().trim(), + status: z.nativeEnum(ExportStatus), + exportFormat: z.nativeEnum(ExportFormat) +}); + +type TFormSchema = z.infer; + +export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: Props) => { + const { + control, + handleSubmit, + watch, + + formState: { errors, isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + exportFormat: ExportFormat.Json, + status: ExportStatus.All, + githubOrganization: "all", + githubRepository: "all" + } + }); + + const selectedOrganization = watch("githubOrganization"); + + const uniqueOrganizations = useMemo(() => { + const organizations = gitRisks.map((risk) => risk.repositoryFullName.split("/")[0]); + + return Array.from(new Set(organizations)); + }, [gitRisks]); + + const uniqueRepositories = useMemo(() => { + const repositories = gitRisks + .filter((risk) => risk.repositoryFullName.split("/")[0] === selectedOrganization) + .map((risk) => risk.repositoryFullName.split("/")[1]); + + return Array.from(new Set(repositories)); + }, [gitRisks, selectedOrganization]); + + const onFormSubmit = async (data: TFormSchema) => { + console.log(data); + const filteredRisks = gitRisks + .filter((risk) => + // eslint-disable-next-line no-nested-ternary + data.status === ExportStatus.All + ? true + : data.status === ExportStatus.Resolved + ? risk.isResolved + : !risk.isResolved + ) + .filter((risk) => { + if (data.githubOrganization === "all") return true; + + if (data.githubRepository === "all") + return risk.repositoryFullName.split("/")[0] === data.githubOrganization; + + return risk.repositoryFullName === `${data.githubOrganization}/${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`); + } + }; + + console.log("errors", errors); + return ( +
+ { + handlePopUpToggle("exportSecretScans", isOpen); + }} + > + handleSubmit(onFormSubmit)()} + isLoading={isSubmitting} + type="submit" + colorSchema="primary" + > + Export secret scans + , + + ]} + > + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + {selectedOrganization && selectedOrganization !== "all" && ( + ( + + + + )} + /> + )} + + +
+ ); +}; diff --git a/frontend/src/pages/organization/SecretScanningPage/components/SecretScanningLogsTable.tsx b/frontend/src/pages/organization/SecretScanningPage/components/SecretScanningLogsTable.tsx index 8ee387b77..c13c5e451 100644 --- a/frontend/src/pages/organization/SecretScanningPage/components/SecretScanningLogsTable.tsx +++ b/frontend/src/pages/organization/SecretScanningPage/components/SecretScanningLogsTable.tsx @@ -12,18 +12,18 @@ import { THead, Tr } from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { useGetSecretScanningRisks } from "@app/hooks/api/secretScanning"; +import { TSecretScanningGitRisks } from "@app/hooks/api/secretScanning/types"; import { RiskStatusSelection } from "./RiskStatusSelection"; -export const SecretScanningLogsTable = () => { - const { currentOrg } = useOrganization(); - const organizationId = currentOrg.id; - const { isPending, data: gitRisks } = useGetSecretScanningRisks(organizationId); +type Props = { + gitRisks?: TSecretScanningGitRisks[]; + isPending: boolean; +}; +export const SecretScanningLogsTable = ({ gitRisks, isPending }: Props) => { return ( - + diff --git a/frontend/src/pages/organization/SecretScanningPage/route.tsx b/frontend/src/pages/organization/SecretScanningPage/route.tsx index d73270db1..4e9260d94 100644 --- a/frontend/src/pages/organization/SecretScanningPage/route.tsx +++ b/frontend/src/pages/organization/SecretScanningPage/route.tsx @@ -1,25 +1,25 @@ -import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; -import { zodValidator } from "@tanstack/zod-adapter"; -import { z } from "zod"; +import { createFileRoute /* 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.string().catch("") -}); +// const SecretScanningQueryParams = z.object({ +// state: z.string().catch(""), +// installation_id: z.string().catch("") +// }); export const Route = createFileRoute( "/_authenticate/_inject-org-details/organization/_layout/secret-scanning" )({ - component: SecretScanningPage, - validateSearch: zodValidator(SecretScanningQueryParams), - search: { - middlewares: [ - stripSearchParams({ - installation_id: "", - state: "" - }) - ] - } + component: SecretScanningPage + // validateSearch: zodValidator(SecretScanningQueryParams), + // search: { + // middlewares: [ + // stripSearchParams({ + // installation_id: "", + // state: "" + // }) + // ] + // } });