mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
feat(radar): export radar data
This commit is contained in:
@ -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");
|
||||
|
48
frontend/src/lib/fn/csv.ts
Normal file
48
frontend/src/lib/fn/csv.ts
Normal 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;" });
|
||||
};
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
@ -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: ""
|
||||
// })
|
||||
// ]
|
||||
// }
|
||||
});
|
||||
|
Reference in New Issue
Block a user