mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #2988 from Infisical/daniel/audit-logs-searchability
feat(radar): pagination and filtering
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
|
||||
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
|
||||
import {
|
||||
SecretScanningResolvedStatus,
|
||||
SecretScanningRiskStatus
|
||||
} from "@app/ee/services/secret-scanning/secret-scanning-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -97,6 +101,45 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/organization/:organizationId/risks/export",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
querystring: z.object({
|
||||
repositoryNames: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? val.split(",") : undefined)),
|
||||
resolvedStatus: z.nativeEnum(SecretScanningResolvedStatus).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
risks: SecretScanningGitRisksSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const risks = await server.services.secretScanning.getAllRisksByOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId,
|
||||
filter: {
|
||||
repositoryNames: req.query.repositoryNames,
|
||||
resolvedStatus: req.query.resolvedStatus
|
||||
}
|
||||
});
|
||||
return { risks };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/organization/:organizationId/risks",
|
||||
method: "GET",
|
||||
@@ -105,20 +148,46 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
},
|
||||
schema: {
|
||||
params: z.object({ organizationId: z.string().trim() }),
|
||||
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
limit: z.coerce.number().min(1).max(20000).default(100),
|
||||
orderBy: z.enum(["createdAt", "name"]).default("createdAt"),
|
||||
orderDirection: z.nativeEnum(OrderByDirection).default(OrderByDirection.DESC),
|
||||
repositoryNames: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? val.split(",") : undefined)),
|
||||
resolvedStatus: z.nativeEnum(SecretScanningResolvedStatus).optional()
|
||||
}),
|
||||
|
||||
response: {
|
||||
200: z.object({ risks: SecretScanningGitRisksSchema.array() })
|
||||
200: z.object({
|
||||
risks: SecretScanningGitRisksSchema.array(),
|
||||
totalCount: z.number(),
|
||||
repos: z.array(z.string())
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { risks } = await server.services.secretScanning.getRisksByOrg({
|
||||
const { risks, totalCount, repos } = await server.services.secretScanning.getRisksByOrg({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.params.organizationId
|
||||
orgId: req.params.organizationId,
|
||||
filter: {
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
orderBy: req.query.orderBy,
|
||||
orderDirection: req.query.orderDirection,
|
||||
repositoryNames: req.query.repositoryNames,
|
||||
resolvedStatus: req.query.resolvedStatus
|
||||
}
|
||||
});
|
||||
return { risks };
|
||||
return { risks, totalCount, repos };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Knex } from "knex";
|
||||
import knex, { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TSecretScanningGitRisksInsert } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
|
||||
import { SecretScanningResolvedStatus, TGetOrgRisksDTO } from "./secret-scanning-types";
|
||||
|
||||
export type TSecretScanningDALFactory = ReturnType<typeof secretScanningDALFactory>;
|
||||
|
||||
@@ -19,5 +22,70 @@ export const secretScanningDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...gitRiskOrm, upsert };
|
||||
const findByOrgId = async (orgId: string, filter: TGetOrgRisksDTO["filter"], tx?: Knex) => {
|
||||
try {
|
||||
// Find statements
|
||||
const sqlQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk)
|
||||
// eslint-disable-next-line func-names
|
||||
.where(`${TableName.SecretScanningGitRisk}.orgId`, orgId);
|
||||
|
||||
if (filter.repositoryNames) {
|
||||
void sqlQuery.whereIn(`${TableName.SecretScanningGitRisk}.repositoryFullName`, filter.repositoryNames);
|
||||
}
|
||||
|
||||
if (filter.resolvedStatus) {
|
||||
if (filter.resolvedStatus !== SecretScanningResolvedStatus.All) {
|
||||
const isResolved = filter.resolvedStatus === SecretScanningResolvedStatus.Resolved;
|
||||
|
||||
void sqlQuery.where(`${TableName.SecretScanningGitRisk}.isResolved`, isResolved);
|
||||
}
|
||||
}
|
||||
|
||||
// Select statements
|
||||
void sqlQuery
|
||||
.select(selectAllTableCols(TableName.SecretScanningGitRisk))
|
||||
.limit(filter.limit)
|
||||
.offset(filter.offset);
|
||||
|
||||
if (filter.orderBy) {
|
||||
const orderDirection = filter.orderDirection || OrderByDirection.ASC;
|
||||
|
||||
void sqlQuery.orderBy(filter.orderBy, orderDirection);
|
||||
}
|
||||
|
||||
const countQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk)
|
||||
.where(`${TableName.SecretScanningGitRisk}.orgId`, orgId)
|
||||
.count();
|
||||
|
||||
const uniqueReposQuery = (tx || db.replicaNode())(TableName.SecretScanningGitRisk)
|
||||
.where(`${TableName.SecretScanningGitRisk}.orgId`, orgId)
|
||||
.distinct("repositoryFullName")
|
||||
.select("repositoryFullName");
|
||||
|
||||
// we timeout long running queries to prevent DB resource issues (2 minutes)
|
||||
const docs = await sqlQuery.timeout(1000 * 120);
|
||||
const uniqueRepos = await uniqueReposQuery.timeout(1000 * 120);
|
||||
const totalCount = await countQuery;
|
||||
|
||||
return {
|
||||
risks: docs,
|
||||
totalCount: Number(totalCount?.[0].count),
|
||||
repos: uniqueRepos
|
||||
.filter(Boolean)
|
||||
.map((r) => r.repositoryFullName!)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof knex.KnexTimeoutError) {
|
||||
throw new GatewayTimeoutError({
|
||||
error,
|
||||
message: "Failed to fetch secret leaks due to timeout. Add more search filters."
|
||||
});
|
||||
}
|
||||
|
||||
throw new DatabaseError({ error });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...gitRiskOrm, upsert, findByOrgId };
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import { TSecretScanningDALFactory } from "./secret-scanning-dal";
|
||||
import { TSecretScanningQueueFactory } from "./secret-scanning-queue";
|
||||
import {
|
||||
SecretScanningRiskStatus,
|
||||
TGetAllOrgRisksDTO,
|
||||
TGetOrgInstallStatusDTO,
|
||||
TGetOrgRisksDTO,
|
||||
TInstallAppSessionDTO,
|
||||
@@ -118,11 +119,21 @@ export const secretScanningServiceFactory = ({
|
||||
return Boolean(appInstallation);
|
||||
};
|
||||
|
||||
const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetOrgRisksDTO) => {
|
||||
const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const results = await secretScanningDAL.findByOrgId(orgId, filter);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
|
||||
return { risks };
|
||||
return risks;
|
||||
};
|
||||
|
||||
const updateRiskStatus = async ({
|
||||
@@ -189,6 +200,7 @@ export const secretScanningServiceFactory = ({
|
||||
linkInstallationToOrg,
|
||||
getOrgInstallationStatus,
|
||||
getRisksByOrg,
|
||||
getAllRisksByOrg,
|
||||
updateRiskStatus,
|
||||
handleRepoPushEvent,
|
||||
handleRepoDeleteEvent
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export enum SecretScanningRiskStatus {
|
||||
FalsePositive = "RESOLVED_FALSE_POSITIVE",
|
||||
@@ -7,6 +7,12 @@ export enum SecretScanningRiskStatus {
|
||||
Unresolved = "UNRESOLVED"
|
||||
}
|
||||
|
||||
export enum SecretScanningResolvedStatus {
|
||||
All = "all",
|
||||
Resolved = "resolved",
|
||||
Unresolved = "unresolved"
|
||||
}
|
||||
|
||||
export type TInstallAppSessionDTO = TOrgPermission;
|
||||
|
||||
export type TLinkInstallSessionDTO = {
|
||||
@@ -16,7 +22,22 @@ export type TLinkInstallSessionDTO = {
|
||||
|
||||
export type TGetOrgInstallStatusDTO = TOrgPermission;
|
||||
|
||||
export type TGetOrgRisksDTO = TOrgPermission;
|
||||
type RiskFilter = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
orderBy?: "createdAt" | "name";
|
||||
orderDirection?: OrderByDirection;
|
||||
repositoryNames?: string[];
|
||||
resolvedStatus?: SecretScanningResolvedStatus;
|
||||
};
|
||||
|
||||
export type TGetOrgRisksDTO = {
|
||||
filter: RiskFilter;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TGetAllOrgRisksDTO = {
|
||||
filter: Omit<RiskFilter, "offset" | "limit" | "orderBy" | "orderDirection">;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TUpdateRiskStatusDTO = {
|
||||
riskId: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
useCreateNewInstallationSession,
|
||||
useExportSecretScanningRisks,
|
||||
useLinkGitAppInstallationWithOrg,
|
||||
useUpdateRiskStatus
|
||||
} from "./mutation";
|
||||
|
||||
@@ -2,7 +2,12 @@ import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { RiskStatus, TGitAppOrg, TSecretScanningGitRisks } from "./types";
|
||||
import {
|
||||
RiskStatus,
|
||||
SecretScanningResolvedStatus,
|
||||
TGitAppOrg,
|
||||
TSecretScanningGitRisks
|
||||
} from "./types";
|
||||
|
||||
export const useCreateNewInstallationSession = () => {
|
||||
return useMutation<{ sessionId: string }, object, { organizationId: string }>({
|
||||
@@ -43,3 +48,31 @@ export const useLinkGitAppInstallationWithOrg = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useExportSecretScanningRisks = () => {
|
||||
return useMutation<
|
||||
TSecretScanningGitRisks[],
|
||||
object,
|
||||
{
|
||||
orgId: string;
|
||||
filter: {
|
||||
repositoryNames?: string[];
|
||||
resolvedStatus?: SecretScanningResolvedStatus;
|
||||
};
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({ filter, orgId }) => {
|
||||
const params = new URLSearchParams({
|
||||
...(filter.resolvedStatus && { resolvedStatus: filter.resolvedStatus }),
|
||||
...(filter.repositoryNames && { repositoryNames: filter.repositoryNames.join(",") })
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<{
|
||||
risks: TSecretScanningGitRisks[];
|
||||
}>(`/api/v1/secret-scanning/organization/${orgId}/risks/export`, {
|
||||
params
|
||||
});
|
||||
return data.risks;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,11 +2,19 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TSecretScanningGitRisks } from "./types";
|
||||
import { SecretScanningOrderBy, SecretScanningRiskFilter, TSecretScanningGitRisks } from "./types";
|
||||
|
||||
export const secretScanningQueryKeys = {
|
||||
getInstallationStatus: (orgId: string) => ["secret-scanning-installation-status", { orgId }],
|
||||
getRisksByOrganizatio: (orgId: string) => ["secret-scanning-risks", { orgId }]
|
||||
getRisksByOrganization: (
|
||||
orgId: string,
|
||||
sort: {
|
||||
offset: number;
|
||||
limit: number;
|
||||
orderBy: SecretScanningOrderBy;
|
||||
},
|
||||
filter: SecretScanningRiskFilter
|
||||
) => ["secret-scanning-risks", { orgId, sort, filter }]
|
||||
};
|
||||
|
||||
const fetchSecretScanningInstallationStatus = async (organizationId: string) => {
|
||||
@@ -22,15 +30,43 @@ export const useGetSecretScanningInstallationStatus = (orgId: string) =>
|
||||
queryFn: () => fetchSecretScanningInstallationStatus(orgId)
|
||||
});
|
||||
|
||||
const fetchSecretScanningRisksByOrgId = async (oranizationId: string) => {
|
||||
const { data } = await apiRequest.get<{ risks: TSecretScanningGitRisks[] }>(
|
||||
`/api/v1/secret-scanning/organization/${oranizationId}/risks`
|
||||
);
|
||||
return data.risks;
|
||||
const fetchSecretScanningRisksByOrgId = async (
|
||||
organizationId: string,
|
||||
sort: {
|
||||
offset: number;
|
||||
limit: number;
|
||||
orderBy: SecretScanningOrderBy;
|
||||
},
|
||||
filter: SecretScanningRiskFilter
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
offset: String(sort.offset),
|
||||
limit: String(sort.limit),
|
||||
orderBy: sort.orderBy,
|
||||
...(filter.resolvedStatus && { resolvedStatus: filter.resolvedStatus }),
|
||||
...(filter.repositoryNames && { repositoryNames: filter.repositoryNames.join(",") })
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<{
|
||||
risks: TSecretScanningGitRisks[];
|
||||
totalCount: number;
|
||||
repos: string[];
|
||||
}>(`/api/v1/secret-scanning/organization/${organizationId}/risks`, {
|
||||
params
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetSecretScanningRisks = (orgId: string) =>
|
||||
export const useGetSecretScanningRisks = (
|
||||
orgId: string,
|
||||
sort: {
|
||||
offset: number;
|
||||
limit: number;
|
||||
orderBy: SecretScanningOrderBy;
|
||||
},
|
||||
filter: SecretScanningRiskFilter
|
||||
) =>
|
||||
useQuery({
|
||||
queryKey: secretScanningQueryKeys.getRisksByOrganizatio(orgId),
|
||||
queryFn: () => fetchSecretScanningRisksByOrgId(orgId)
|
||||
queryKey: secretScanningQueryKeys.getRisksByOrganization(orgId, sort, filter),
|
||||
queryFn: () => fetchSecretScanningRisksByOrgId(orgId, sort, filter)
|
||||
});
|
||||
|
||||
@@ -5,6 +5,21 @@ export enum RiskStatus {
|
||||
UNRESOLVED = "UNRESOLVED"
|
||||
}
|
||||
|
||||
export enum SecretScanningOrderBy {
|
||||
CreatedAt = "createdAt"
|
||||
}
|
||||
|
||||
export enum SecretScanningResolvedStatus {
|
||||
All = "all",
|
||||
Resolved = "resolved",
|
||||
Unresolved = "unresolved"
|
||||
}
|
||||
|
||||
export type SecretScanningRiskFilter = {
|
||||
repositoryNames?: string[];
|
||||
resolvedStatus?: SecretScanningResolvedStatus;
|
||||
};
|
||||
|
||||
export type TSecretScanningGitRisks = {
|
||||
id: string;
|
||||
description: string;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, NoticeBanner } from "@app/components/v2";
|
||||
import { Button, NoticeBanner, Pagination } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
@@ -12,28 +14,58 @@ import {
|
||||
useServerConfig
|
||||
} from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePagination, usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateNewInstallationSession,
|
||||
useGetSecretScanningInstallationStatus,
|
||||
useGetSecretScanningRisks,
|
||||
useLinkGitAppInstallationWithOrg
|
||||
} from "@app/hooks/api/secretScanning";
|
||||
import { SecretScanningOrderBy } from "@app/hooks/api/secretScanning/types";
|
||||
|
||||
import { ExportSecretScansModal } from "./components/ExportSecretScansModal";
|
||||
import { SecretScanningFilter } from "./components/SecretScanningFilters";
|
||||
import { SecretScanningFilterFormData, secretScanningFilterFormSchema } from "./components/types";
|
||||
import { SecretScanningLogsTable } from "./components";
|
||||
|
||||
const PER_PAGE_INIT = 25;
|
||||
|
||||
export const SecretScanningPage = withPermission(
|
||||
() => {
|
||||
const queryParams = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SecretScanning.id
|
||||
});
|
||||
|
||||
const { control, watch } = useForm<SecretScanningFilterFormData>({
|
||||
resolver: zodResolver(secretScanningFilterFormSchema),
|
||||
defaultValues: {}
|
||||
});
|
||||
|
||||
const { config } = useServerConfig();
|
||||
const { currentOrg } = useOrganization();
|
||||
const organizationId = currentOrg.id;
|
||||
|
||||
const { isPending, data: gitRisks } = useGetSecretScanningRisks(organizationId);
|
||||
const { offset, limit, orderBy, setPage, perPage, page, setPerPage } = usePagination(
|
||||
SecretScanningOrderBy.CreatedAt,
|
||||
{ initPerPage: PER_PAGE_INIT }
|
||||
);
|
||||
|
||||
const repositoryNames = watch("repositoryNames");
|
||||
const resolvedStatus = watch("resolved");
|
||||
|
||||
const { isPending, data: risksData } = useGetSecretScanningRisks(
|
||||
organizationId,
|
||||
{
|
||||
offset,
|
||||
limit,
|
||||
orderBy
|
||||
},
|
||||
{
|
||||
repositoryNames:
|
||||
repositoryNames?.length > 0 ? repositoryNames.map((repo) => repo.name) : undefined,
|
||||
resolvedStatus
|
||||
}
|
||||
);
|
||||
|
||||
const { mutateAsync: linkGitAppInstallationWithOrganization } =
|
||||
useLinkGitAppInstallationWithOrg();
|
||||
@@ -72,7 +104,7 @@ export const SecretScanningPage = withPermission(
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="py-2">
|
||||
<Helmet>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
@@ -141,21 +173,33 @@ export const SecretScanningPage = withPermission(
|
||||
)}
|
||||
</div>
|
||||
<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} />
|
||||
{integrationEnabled && (
|
||||
<div className="flex w-full items-center justify-end">
|
||||
<SecretScanningFilter
|
||||
repositories={risksData?.repos || []}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<SecretScanningLogsTable gitRisks={risksData?.risks} isPending={isPending} />
|
||||
{!isPending &&
|
||||
risksData?.totalCount !== undefined &&
|
||||
risksData.totalCount >= PER_PAGE_INIT && (
|
||||
<Pagination
|
||||
className="rounded-md"
|
||||
count={risksData.totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ExportSecretScansModal
|
||||
gitRisks={gitRisks || []}
|
||||
repositories={risksData?.repos || []}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
popUp={popUp}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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 { useOrganization } from "@app/context";
|
||||
import { useExportSecretScanningRisks } from "@app/hooks/api/secretScanning";
|
||||
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { convertJsonToCsv } from "@app/lib/fn/csv";
|
||||
|
||||
@@ -15,7 +16,7 @@ type Props = {
|
||||
popUpName: keyof UsePopUpState<["exportSecretScans"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
gitRisks: TSecretScanningGitRisks[];
|
||||
repositories: string[];
|
||||
};
|
||||
|
||||
enum ExportFormat {
|
||||
@@ -23,71 +24,52 @@ enum ExportFormat {
|
||||
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),
|
||||
status: z.nativeEnum(SecretScanningResolvedStatus),
|
||||
exportFormat: z.nativeEnum(ExportFormat)
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: Props) => {
|
||||
export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, repositories }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync } = useExportSecretScanningRisks();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
exportFormat: ExportFormat.Json,
|
||||
status: ExportStatus.All,
|
||||
githubOrganization: "all",
|
||||
status: SecretScanningResolvedStatus.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 gitRisks = await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
filter: {
|
||||
repositoryNames: data.githubRepository !== "all" ? [data.githubRepository] : undefined,
|
||||
resolvedStatus: data.status
|
||||
}
|
||||
});
|
||||
|
||||
const filteredRisks = gitRisks
|
||||
.filter((risk) =>
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
data.status === ExportStatus.All
|
||||
data.status === SecretScanningResolvedStatus.All
|
||||
? true
|
||||
: data.status === ExportStatus.Resolved
|
||||
: data.status === SecretScanningResolvedStatus.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}`;
|
||||
if (data.githubRepository === "all") return true;
|
||||
return risk.repositoryFullName === data.githubRepository;
|
||||
});
|
||||
|
||||
const formattedRisks = filteredRisks.map((risk) => {
|
||||
@@ -148,27 +130,6 @@ export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: P
|
||||
</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"
|
||||
@@ -192,19 +153,18 @@ export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: P
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="githubOrganization"
|
||||
name="status"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="GitHub Organization">
|
||||
<FormControl label="Risk Status">
|
||||
<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}
|
||||
{Object.values(SecretScanningResolvedStatus).map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -212,29 +172,27 @@ export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, gitRisks }: P
|
||||
)}
|
||||
/>
|
||||
|
||||
{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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
{repositories.map((repoName) => (
|
||||
<SelectItem key={repoName} value={repoName}>
|
||||
{repoName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SecretScanningFilterFormData } from "./types";
|
||||
|
||||
type Props = {
|
||||
control: Control<SecretScanningFilterFormData>;
|
||||
repositories: string[];
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["exportSecretScans"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const SecretScanningFilter = ({ repositories, control, handlePopUpToggle }: Props) => {
|
||||
return (
|
||||
<div className={twMerge("flex w-full flex-wrap items-center justify-between bg-bunker-800")}>
|
||||
<div className="flex items-center -space-x-8">
|
||||
<Controller
|
||||
control={control}
|
||||
name="repositoryNames"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Repository"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-12 w-96"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
isMulti
|
||||
onChange={onChange}
|
||||
placeholder="Select a repository..."
|
||||
options={repositories.map((repository) => ({
|
||||
name: repository
|
||||
}))}
|
||||
getOptionValue={(option) => option.name}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="resolved"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Status"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-12 w-44"
|
||||
>
|
||||
<Select
|
||||
defaultValue={SecretScanningResolvedStatus.All}
|
||||
placeholder={SecretScanningResolvedStatus.All}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full"
|
||||
>
|
||||
{Object.values(SecretScanningResolvedStatus).map((status) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)} risks
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
className="mt-[0.45rem]"
|
||||
onClick={() => handlePopUpToggle("exportSecretScans", true)}
|
||||
variant="solid"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
|
||||
|
||||
export const secretScanningFilterFormSchema = z.object({
|
||||
repositoryNames: z.array(z.object({ name: z.string() })),
|
||||
resolved: z
|
||||
.nativeEnum(SecretScanningResolvedStatus)
|
||||
.default(SecretScanningResolvedStatus.All)
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type SecretScanningFilterFormData = z.infer<typeof secretScanningFilterFormSchema>;
|
||||
|
||||
export type SetValueType = (
|
||||
name: keyof SecretScanningFilterFormData,
|
||||
value: any,
|
||||
options?: {
|
||||
shouldValidate?: boolean;
|
||||
shouldDirty?: boolean;
|
||||
}
|
||||
) => void;
|
||||
Reference in New Issue
Block a user