mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 22:26:41 +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 { 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,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
|
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/ExportResultsModal";
|
||||||
import { SecretScanningLogsTable } from "./components";
|
import { SecretScanningLogsTable } from "./components";
|
||||||
|
|
||||||
export const SecretScanningPage = withPermission(
|
export const SecretScanningPage = withPermission(
|
||||||
() => {
|
() => {
|
||||||
const queryParams = useSearch({
|
const queryParams = useSearch({
|
||||||
from: ROUTE_PATHS.Organization.SecretScanning.id
|
from: ROUTE_PATHS.Organization.SecretScanning.id
|
||||||
});
|
}) as Record<string, string>;
|
||||||
|
|
||||||
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();
|
||||||
@@ -60,7 +68,7 @@ export const SecretScanningPage = withPermission(
|
|||||||
|
|
||||||
const generateNewIntegrationSession = async () => {
|
const generateNewIntegrationSession = async () => {
|
||||||
const session = await createNewIntegrationSession({ organizationId });
|
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 (
|
return (
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+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>
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
import { createFileRoute /* stripSearchParams */ } from "@tanstack/react-router";
|
||||||
import { zodValidator } from "@tanstack/zod-adapter";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
|
// import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
// import { z } from "zod";
|
||||||
import { SecretScanningPage } from "./SecretScanningPage";
|
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.string().catch("")
|
||||||
});
|
// });
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning"
|
"/_authenticate/_inject-org-details/organization/_layout/secret-scanning"
|
||||||
)({
|
)({
|
||||||
component: SecretScanningPage,
|
component: SecretScanningPage
|
||||||
validateSearch: zodValidator(SecretScanningQueryParams),
|
// validateSearch: zodValidator(SecretScanningQueryParams),
|
||||||
search: {
|
// search: {
|
||||||
middlewares: [
|
// middlewares: [
|
||||||
stripSearchParams({
|
// stripSearchParams({
|
||||||
installation_id: "",
|
// installation_id: "",
|
||||||
state: ""
|
// state: ""
|
||||||
})
|
// })
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user