mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 16:27:40 +00:00
Merge pull request #2981 from Infisical/daniel/secret-scans-export
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 { PushEvent } from "@octokit/webhooks-types";
|
||||||
import { Probot } from "probot";
|
import { Probot } from "probot";
|
||||||
import SmeeClient from "smee-client";
|
import SmeeClient from "smee-client";
|
||||||
@@ -54,14 +55,14 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
handler: async (req, res) => {
|
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 signatureSHA256 = req.headers["x-hub-signature-256"] as string;
|
||||||
const id = req.headers["x-github-delivery"] as string;
|
const id = req.headers["x-github-delivery"] as string;
|
||||||
|
|
||||||
await probot.webhooks.verifyAndReceive({
|
await probot.webhooks.verifyAndReceive({
|
||||||
id,
|
id,
|
||||||
// @ts-expect-error type
|
|
||||||
name: eventName,
|
name: eventName,
|
||||||
payload: req.body as string,
|
payload: JSON.stringify(req.body),
|
||||||
signature: signatureSHA256
|
signature: signatureSHA256
|
||||||
});
|
});
|
||||||
void res.send("ok");
|
void res.send("ok");
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Converts a JSON array of objects to CSV format
|
||||||
|
* @param {Array} jsonData - Array of objects to convert
|
||||||
|
*/
|
||||||
|
export const convertJsonToCsv = (jsonData: Record<string, string | number | boolean>[]) => {
|
||||||
|
if (jsonData.length === 0) {
|
||||||
|
return new Blob([""], { type: "text/csv;charset=utf-8;" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = Object.keys(jsonData[0]);
|
||||||
|
|
||||||
|
const csvRows = [
|
||||||
|
headers.join(","),
|
||||||
|
...jsonData.map((row) => {
|
||||||
|
return headers
|
||||||
|
.map((header) => {
|
||||||
|
let cell = row[header];
|
||||||
|
|
||||||
|
if (cell === null || cell === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof cell === "number" || typeof cell === "boolean") {
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,12 +12,15 @@ import {
|
|||||||
useServerConfig
|
useServerConfig
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateNewInstallationSession,
|
useCreateNewInstallationSession,
|
||||||
useGetSecretScanningInstallationStatus,
|
useGetSecretScanningInstallationStatus,
|
||||||
|
useGetSecretScanningRisks,
|
||||||
useLinkGitAppInstallationWithOrg
|
useLinkGitAppInstallationWithOrg
|
||||||
} from "@app/hooks/api/secretScanning";
|
} from "@app/hooks/api/secretScanning";
|
||||||
|
|
||||||
|
import { ExportSecretScansModal } from "./components/ExportSecretScansModal";
|
||||||
import { SecretScanningLogsTable } from "./components";
|
import { SecretScanningLogsTable } from "./components";
|
||||||
|
|
||||||
export const SecretScanningPage = withPermission(
|
export const SecretScanningPage = withPermission(
|
||||||
@@ -25,10 +28,13 @@ export const SecretScanningPage = withPermission(
|
|||||||
const queryParams = useSearch({
|
const queryParams = useSearch({
|
||||||
from: ROUTE_PATHS.Organization.SecretScanning.id
|
from: ROUTE_PATHS.Organization.SecretScanning.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const { config } = useServerConfig();
|
const { config } = useServerConfig();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const organizationId = currentOrg.id;
|
const organizationId = currentOrg.id;
|
||||||
|
|
||||||
|
const { isPending, data: gitRisks } = useGetSecretScanningRisks(organizationId);
|
||||||
|
|
||||||
const { mutateAsync: linkGitAppInstallationWithOrganization } =
|
const { mutateAsync: linkGitAppInstallationWithOrganization } =
|
||||||
useLinkGitAppInstallationWithOrg();
|
useLinkGitAppInstallationWithOrg();
|
||||||
const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession();
|
const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession();
|
||||||
@@ -37,13 +43,15 @@ export const SecretScanningPage = withPermission(
|
|||||||
const integrationEnabled =
|
const integrationEnabled =
|
||||||
!isSecretScanningInstatllationStatusLoading && installationStatus?.appInstallationCompleted;
|
!isSecretScanningInstatllationStatusLoading && installationStatus?.appInstallationCompleted;
|
||||||
|
|
||||||
|
const { handlePopUpToggle, popUp } = usePopUp(["exportSecretScans"]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const linkInstallation = async () => {
|
const linkInstallation = async () => {
|
||||||
if (queryParams.state && queryParams.installation_id) {
|
if (queryParams.state && queryParams.installation_id) {
|
||||||
try {
|
try {
|
||||||
const isLinked = await linkGitAppInstallationWithOrganization({
|
const isLinked = await linkGitAppInstallationWithOrganization({
|
||||||
installationId: queryParams.installation_id as string,
|
installationId: String(queryParams.installation_id),
|
||||||
sessionId: queryParams.state as string
|
sessionId: String(queryParams.state)
|
||||||
});
|
});
|
||||||
if (isLinked) {
|
if (isLinked) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -132,9 +140,25 @@ export const SecretScanningPage = withPermission(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
<ExportSecretScansModal
|
||||||
|
gitRisks={gitRisks || []}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
popUp={popUp}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+242
@@ -0,0 +1,242 @@
|
|||||||
|
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: { 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) => {
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+7
-7
@@ -12,18 +12,18 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { TSecretScanningGitRisks } from "@app/hooks/api/secretScanning/types";
|
||||||
import { useGetSecretScanningRisks } from "@app/hooks/api/secretScanning";
|
|
||||||
|
|
||||||
import { RiskStatusSelection } from "./RiskStatusSelection";
|
import { RiskStatusSelection } from "./RiskStatusSelection";
|
||||||
|
|
||||||
export const SecretScanningLogsTable = () => {
|
type Props = {
|
||||||
const { currentOrg } = useOrganization();
|
gitRisks?: TSecretScanningGitRisks[];
|
||||||
const organizationId = currentOrg.id;
|
isPending: boolean;
|
||||||
const { isPending, data: gitRisks } = useGetSecretScanningRisks(organizationId);
|
};
|
||||||
|
|
||||||
|
export const SecretScanningLogsTable = ({ gitRisks, isPending }: Props) => {
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-8">
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { SecretScanningPage } from "./SecretScanningPage";
|
|||||||
|
|
||||||
const SecretScanningQueryParams = z.object({
|
const SecretScanningQueryParams = z.object({
|
||||||
state: z.string().catch(""),
|
state: z.string().catch(""),
|
||||||
installation_id: z.string().catch("")
|
installation_id: z.coerce.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
|
|||||||
Reference in New Issue
Block a user