feat(radar): export radar data

This commit is contained in:
Daniel Hougaard
2025-01-14 00:53:40 +01:00
parent a4aa65bb81
commit ab3ee775bb
6 changed files with 349 additions and 32 deletions

View File

@ -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");

View File

@ -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<string, string | number | boolean>[]) => {
// 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;" });
};

View File

@ -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<string, string>;
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(
</div>
)}
</div>
<SecretScanningLogsTable />
<div className="mt-8 space-y-3">
<div className="flex w-full justify-end">
<Button
onClick={() => handlePopUpToggle("exportSecretScans", true)}
variant="solid"
colorSchema="secondary"
>
Export
</Button>
</div>
<SecretScanningLogsTable gitRisks={gitRisks} isPending={isPending} />
</div>
</div>
</div>
<ExportSecretScansModal
gitRisks={gitRisks || []}
handlePopUpToggle={handlePopUpToggle}
popUp={popUp}
/>
</div>
);
},

View File

@ -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<typeof formSchema>;
export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: Props) => {
const {
control,
handleSubmit,
watch,
formState: { errors, isSubmitting }
} = useForm<TFormSchema>({
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 (
<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="status"
render={({ field: { value, onChange } }) => (
<FormControl label="Risk Status">
<Select
defaultValue="all"
value={value}
onValueChange={onChange}
className="w-full"
>
{Object.values(ExportStatus).map((status) => (
<SelectItem key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<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="githubOrganization"
render={({ field: { value, onChange } }) => (
<FormControl label="GitHub Organization">
<Select
defaultValue="all"
value={value}
onValueChange={onChange}
className="w-full"
>
<SelectItem value="all">All Organizations</SelectItem>
{uniqueOrganizations.map((orgName) => (
<SelectItem key={orgName} value={orgName}>
{orgName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{selectedOrganization && selectedOrganization !== "all" && (
<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>
{uniqueRepositories.map((repoName) => (
<SelectItem key={repoName} value={repoName}>
{repoName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
</ModalContent>
</Modal>
</form>
);
};

View File

@ -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 (
<TableContainer className="mt-8">
<TableContainer>
<Table>
<THead>
<Tr>

View File

@ -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: ""
// })
// ]
// }
});