feat(audit-logs): filtering revamp

This commit is contained in:
Daniel Hougaard
2025-03-27 09:41:46 +04:00
parent fb0b6b00dd
commit 53c015988d
17 changed files with 615 additions and 391 deletions
@@ -9,13 +9,14 @@ import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { EventType } from "./audit-log-types"; import { EventType, filterableSecretEvents } from "./audit-log-types";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>; export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
type TFindQuery = { type TFindQuery = {
actor?: string; actor?: string;
projectId?: string; projectId?: string;
environment?: string;
orgId?: string; orgId?: string;
eventType?: string; eventType?: string;
startDate?: string; startDate?: string;
@@ -32,6 +33,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
{ {
orgId, orgId,
projectId, projectId,
environment,
userAgentType, userAgentType,
startDate, startDate,
endDate, endDate,
@@ -92,7 +94,13 @@ export const auditLogDALFactory = (db: TDbClient) => {
}); });
} }
if (projectId) { const eventIsSecretType = !eventType?.length || eventType.some((event) => filterableSecretEvents.includes(event));
// We only want to filter for environment/secretPath/secretKey if the user is either checking for all event types
if (projectId && eventIsSecretType) {
if (environment) {
void sqlQuery.whereRaw(`"eventMetadata"->>'environment' = ?`, [environment]);
}
if (secretPath) { if (secretPath) {
void sqlQuery.whereRaw(`"eventMetadata"->>'secretPath' = ?`, [secretPath]); void sqlQuery.whereRaw(`"eventMetadata"->>'secretPath' = ?`, [secretPath]);
} }
@@ -64,6 +64,7 @@ export const auditLogServiceFactory = ({
eventMetadata: filter.eventMetadata, eventMetadata: filter.eventMetadata,
secretPath: filter.secretPath, secretPath: filter.secretPath,
secretKey: filter.secretKey, secretKey: filter.secretKey,
environment: filter.environment,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
}); });
@@ -287,6 +287,16 @@ export enum EventType {
KMIP_OPERATION_REGISTER = "kmip-operation-register" KMIP_OPERATION_REGISTER = "kmip-operation-register"
} }
export const filterableSecretEvents: EventType[] = [
EventType.GET_SECRET,
EventType.DELETE_SECRETS,
EventType.CREATE_SECRETS,
EventType.UPDATE_SECRETS,
EventType.CREATE_SECRET,
EventType.UPDATE_SECRET,
EventType.DELETE_SECRET
];
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
email?: string | null; email?: string | null;
+2
View File
@@ -840,6 +840,8 @@ export const AUDIT_LOGS = {
EXPORT: { EXPORT: {
projectId: projectId:
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.", "Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
environment:
"The environment to filter logs by. If not provided, logs from all environments will be returned. Note that the projectId parameter must also be provided.",
eventType: "The type of the event to export.", eventType: "The type of the event to export.",
secretPath: secretPath:
"The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.", "The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
@@ -112,6 +112,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
description: "Get all audit logs for an organization", description: "Get all audit logs for an organization",
querystring: z.object({ querystring: z.object({
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment),
actorType: z.nativeEnum(ActorType).optional(), actorType: z.nativeEnum(ActorType).optional(),
secretPath: z secretPath: z
.string() .string()
@@ -1,5 +1,16 @@
import { EventType, UserAgentType } from "./enums"; import { EventType, UserAgentType } from "./enums";
export const secretEvents: EventType[] = [
EventType.GET_SECRETS,
EventType.GET_SECRET,
EventType.DELETE_SECRETS,
EventType.CREATE_SECRETS,
EventType.UPDATE_SECRETS,
EventType.CREATE_SECRET,
EventType.UPDATE_SECRET,
EventType.DELETE_SECRET
];
export const eventToNameMap: { [K in EventType]: string } = { export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_SECRETS]: "List secrets", [EventType.GET_SECRETS]: "List secrets",
[EventType.GET_SECRET]: "Read secret", [EventType.GET_SECRET]: "Read secret",
+2 -2
View File
@@ -12,8 +12,8 @@ export enum UserAgentType {
CLI = "cli", CLI = "cli",
K8_OPERATOR = "k8-operator", K8_OPERATOR = "k8-operator",
TERRAFORM = "terraform", TERRAFORM = "terraform",
NODE_SDK = "node-sdk", NODE_SDK = "InfisicalNodeSDK",
PYTHON_SDK = "python-sdk", PYTHON_SDK = "InfisicalPythonSDK",
OTHER = "other" OTHER = "other"
} }
@@ -9,6 +9,7 @@ export type TGetAuditLogsFilter = {
eventMetadata?: Record<string, string>; eventMetadata?: Record<string, string>;
actorType?: ActorType; actorType?: ActorType;
projectId?: string; projectId?: string;
environment?: string;
actor?: string; // user ID format actor?: string; // user ID format
secretPath?: string; secretPath?: string;
secretKey?: string; secretKey?: string;
@@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
title="Audit logs" title="Audit logs"
description="Audit logs for security and compliance teams to monitor information access." description="Audit logs for security and compliance teams to monitor information access."
/> />
<LogsSection filterClassName="static py-2" showFilters /> <LogsSection />
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,32 @@
import { twMerge } from "tailwind-merge";
import { Button, Tooltip } from "@app/components/v2";
type Props = {
hoverTooltip?: string;
className?: string;
label: string;
onClear: () => void;
children: React.ReactNode;
};
export const LogFilterItem = ({ label, onClear, hoverTooltip, children, className }: Props) => {
return (
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
<div className={twMerge("flex flex-col justify-between", className)}>
<div className="flex items-center justify-between pr-1">
<p className="text-xs opacity-60">{label}</p>
<Button
onClick={() => onClear()}
variant="link"
className="font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
size="xs"
>
Clear
</Button>
</div>
{children}
</div>
</Tooltip>
);
};
@@ -1,10 +1,26 @@
import { useState } from "react"; /* eslint-disable no-nested-ternary */
import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form"; import { useMemo, useState } from "react";
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons"; import {
Control,
Controller,
UseFormGetFieldState,
UseFormReset,
UseFormResetField,
UseFormSetValue,
UseFormWatch
} from "react-hook-form";
import {
faArrowRight,
faCaretDown,
faCheckCircle,
faFilterCircleXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { import {
Badge,
Button, Button,
DatePicker, DatePicker,
DropdownMenu, DropdownMenu,
@@ -18,13 +34,17 @@ import {
SelectItem SelectItem
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/api"; import { useGetUserWorkspaces } from "@app/hooks/api";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; import {
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; eventToNameMap,
import { Actor } from "@app/hooks/api/auditLogs/types"; secretEvents,
import { ProjectType } from "@app/hooks/api/workspace/types"; userAgentTTypeoNameMap
} from "@app/hooks/api/auditLogs/constants";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { UserAgentType } from "@app/hooks/api/auth/types";
import { AuditLogFilterFormData } from "./types"; import { LogFilterItem } from "./LogFilterItem";
import { AuditLogFilterFormData, Presets } from "./types";
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value })); const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
@@ -33,26 +53,70 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
})); }));
type Props = { type Props = {
presets?: { presets?: Presets;
actorId?: string;
eventType?: EventType[];
};
className?: string;
isOrgAuditLogs?: boolean;
setValue: UseFormSetValue<AuditLogFilterFormData>;
control: Control<AuditLogFilterFormData>; control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>; reset: UseFormReset<AuditLogFilterFormData>;
resetField: UseFormResetField<AuditLogFilterFormData>;
watch: UseFormWatch<AuditLogFilterFormData>; watch: UseFormWatch<AuditLogFilterFormData>;
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>;
setValue: UseFormSetValue<AuditLogFilterFormData>;
};
const getActiveFilterCount = (
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>,
watch: UseFormWatch<AuditLogFilterFormData>
) => {
const fields = [
"actor",
"project",
"eventType",
"startDate",
"endDate",
"environment",
"secretPath",
"userAgentType",
"secretKey"
] as Partial<keyof AuditLogFilterFormData>[];
let filterCount = 0;
// either start or end date should only be counted as one filter
let dateProcessed = false;
fields.forEach((field) => {
const fieldState = getFieldState(field);
if (
field === "userAgentType" ||
field === "environment" ||
field === "secretKey" ||
field === "secretPath"
) {
const value = watch(field);
if (value !== undefined && value !== "") {
filterCount += 1;
}
} else if (fieldState.isDirty && !dateProcessed) {
filterCount += 1;
if (field === "startDate" || field === "endDate") {
dateProcessed = true;
}
}
});
return filterCount;
}; };
export const LogsFilter = ({ export const LogsFilter = ({
presets, presets,
isOrgAuditLogs,
className,
control, control,
reset, reset,
setValue, resetField,
watch watch,
getFieldState,
setValue
}: Props) => { }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false); const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
@@ -62,317 +126,423 @@ export const LogsFilter = ({
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id); const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
const { data, isPending } = useGetAuditLogActorFilterOpts(workspaces?.[0]?.id ?? "");
const renderActorSelectItem = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<SelectItem
value={`${actor.type}-${actor.metadata.userId}`}
key={`user-actor-filter-${actor.metadata.userId}`}
>
{actor.metadata.email}
</SelectItem>
);
case ActorType.SERVICE:
return (
<SelectItem
value={`${actor.type}-${actor.metadata.serviceId}`}
key={`service-actor-filter-${actor.metadata.serviceId}`}
>
{actor.metadata.name}
</SelectItem>
);
case ActorType.IDENTITY:
return (
<SelectItem
value={`${actor.type}-${actor.metadata.identityId}`}
key={`identity-filter-${actor.metadata.identityId}`}
>
{actor.metadata.name}
</SelectItem>
);
case ActorType.KMIP_CLIENT:
return (
<SelectItem
value={`${actor.type}-${actor.metadata.clientId}`}
key={`kmip-client-filter-${actor.metadata.clientId}`}
>
{actor.metadata.name}
</SelectItem>
);
default:
return (
<SelectItem value="actor-none" key="actor-none">
N/A
</SelectItem>
);
}
};
const selectedEventTypes = watch("eventType") as EventType[] | undefined; const selectedEventTypes = watch("eventType") as EventType[] | undefined;
const selectedProject = watch("project"); const selectedProject = watch("project");
const showSecretsSection =
selectedEventTypes?.some(
(eventType) => secretEvents.includes(eventType) && eventType !== EventType.GET_SECRETS
) || selectedEventTypes?.length === 0;
const availableEnvironments = useMemo(() => {
if (!selectedProject) return [];
return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? [];
}, [selectedProject, workspacesInOrg]);
const activeFilterCount = getActiveFilterCount(getFieldState, watch);
return ( return (
<div className={twMerge("sticky top-20 z-10 flex flex-col bg-bunker-800", className)}> <DropdownMenu>
<div className="flex items-center gap-4"> <DropdownMenuTrigger asChild>
{isOrgAuditLogs && workspacesInOrg.length > 0 && ( <Button variant="outline_bg" colorSchema="primary">
<Controller <FontAwesomeIcon icon={faFilterCircleXmark} className="mr-3 px-[0.1rem]" />
control={control} Filters
name="project" {activeFilterCount > 0 && (
render={({ field: { onChange, value }, fieldState: { error } }) => ( <Badge className="ml-2 px-1.5 py-0.5" variant="primary">
<FormControl {activeFilterCount}
label="Project" </Badge>
errorText={error?.message}
isError={Boolean(error)}
className="w-64"
>
<FilterableSelect
value={value}
isClearable
onChange={(e) => {
if (e === null) {
setValue("secretPath", "");
}
onChange(e);
}}
placeholder="Select a project..."
options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
)}
{selectedProject?.type === ProjectType.SecretManager && (
<Controller
control={control}
name="secretPath"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
label="Secret Path"
className="w-40"
>
<Input
placeholder="/folder"
{...field}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
)}
{selectedProject?.type === ProjectType.SecretManager && (
<Controller
control={control}
name="secretKey"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to a specific secret."
label="Secret Name"
className="w-40"
>
<Input
{...field}
placeholder="API_KEY"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
)}
</div>
<div className="mt-1 flex w-full justify-start">
<div className="flex items-center space-x-2">
<Controller
control={control}
name="eventType"
render={({ field }) => (
<FormControl label="Events">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{selectedEventTypes?.length === 1
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
?.label
: selectedEventTypes?.length === 0
? "All events"
: `${selectedEventTypes?.length} events selected`}
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[100] max-h-80 overflow-hidden">
<div className="max-h-80 overflow-y-auto">
{eventTypes && eventTypes.length > 0 ? (
eventTypes.map((eventType) => {
const isSelected = selectedEventTypes?.includes(
eventType.value as EventType
);
return (
<DropdownMenuItem
onSelect={(event) => eventTypes.length > 1 && event.preventDefault()}
onClick={() => {
if (selectedEventTypes?.includes(eventType.value as EventType)) {
field.onChange(
selectedEventTypes?.filter((e: string) => e !== eventType.value)
);
} else {
field.onChange([...(selectedEventTypes || []), eventType.value]);
}
}}
key={`event-type-${eventType.value}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{eventType.label}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
{!isPending && data && data.length > 0 && !presets?.actorId && (
<Controller
control={control}
name="actor"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Actor"
errorText={error?.message}
isError={Boolean(error)}
className="w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
>
{data.map((actor) => renderActorSelectItem(actor))}
</Select>
</FormControl>
)}
/>
)} )}
<Controller </Button>
control={control} </DropdownMenuTrigger>
name="userAgentType" <DropdownMenuContent align="end" className="mt-4 py-4">
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => ( <div className="flex min-w-64 flex-col font-inter">
<FormControl <div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
label="Source" <div className="flex w-full items-center justify-between">
errorText={error?.message} <div className="flex items-center gap-2">
isError={Boolean(error)} <span>Filters</span>
className="w-40" <Badge className="px-1.5 py-0.5" variant="primary">
{activeFilterCount}
</Badge>
</div>
<Button
onClick={() => {
reset({
eventType: presets?.eventType || [],
actor: presets?.actorId,
userAgentType: undefined,
startDate: undefined,
endDate: undefined,
project: null,
secretPath: undefined,
secretKey: undefined
});
}}
variant="link"
className="text-mineshaft-400"
size="xs"
> >
<Select Clear filters
value={value === undefined ? "all" : value} </Button>
{...field} </div>
onValueChange={(e) => { </div>
if (e === "all") onChange(undefined);
else onChange(e); <div className="px-3">
<LogFilterItem
label="Events"
onClear={() => {
resetField("eventType");
}}
>
<Controller
control={control}
name="eventType"
render={({ field }) => (
<FormControl>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{selectedEventTypes?.length === 1
? eventTypes.find(
(eventType) => eventType.value === selectedEventTypes[0]
)?.label
: selectedEventTypes?.length === 0
? "All events"
: `${selectedEventTypes?.length} events selected`}
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
>
<div className="max-h-80 overflow-y-auto">
{eventTypes && eventTypes.length > 0 ? (
eventTypes.map((eventType) => {
const isSelected = selectedEventTypes?.includes(
eventType.value as EventType
);
return (
<DropdownMenuItem
onSelect={(event) =>
eventTypes.length > 1 && event.preventDefault()
}
onClick={() => {
if (
selectedEventTypes?.includes(eventType.value as EventType)
) {
field.onChange(
selectedEventTypes?.filter(
(e: string) => e !== eventType.value
)
);
} else {
field.onChange([
...(selectedEventTypes || []),
eventType.value
]);
}
}}
key={`event-type-${eventType.value}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{eventType.label}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Source"
onClear={() => {
resetField("userAgentType");
}}
>
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="w-full"
>
<Select
value={value === undefined ? "all" : value}
{...field}
onValueChange={(e) => {
if (e === "all") onChange(undefined);
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
}}
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
>
<SelectItem value="all" key="all">
All sources
</SelectItem>
{userAgentTypes.map(({ label, value: userAgent }) => (
<SelectItem value={userAgent} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Date"
onClear={() => {
resetField("startDate");
resetField("endDate");
}}
>
<div className="flex h-10 w-full items-center justify-between gap-2">
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}} }}
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")} />
<div className="flex items-center -space-x-3">
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
</div>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div>
</LogFilterItem>
<AnimatePresence initial={false}>
{showSecretsSection && (
<motion.div
className="mt-2 overflow-hidden"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
> >
<SelectItem value="all" key="all"> <div className="mb-3 mt-2">
All sources <p className="text-xs opacity-60">Secrets</p>
</SelectItem> <div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
{userAgentTypes.map(({ label, value: userAgent }) => ( </div>
<SelectItem value={userAgent} key={label}>
{label} <LogFilterItem
</SelectItem> label="Project"
))} onClear={() => {
</Select> resetField("project");
</FormControl> resetField("environment");
)} setValue("secretPath", "");
/> setValue("secretKey", "");
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl label="Start Date" errorText={error?.message} isError={Boolean(error)}>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}} }}
popUpContentProps={{}} >
/> <Controller
</FormControl> control={control}
); name="project"
}} render={({ field: { onChange, value }, fieldState: { error } }) => (
/> <FormControl
<Controller errorText={error?.message}
name="endDate" isError={Boolean(error)}
control={control} className="w-full"
render={({ field: { onChange, ...field }, fieldState: { error } }) => { >
return ( <FilterableSelect
<FormControl label="End Date" errorText={error?.message} isError={Boolean(error)}> value={value}
<DatePicker isClearable
value={field.value || undefined} onChange={(e) => {
onChange={onChange} if (e === null) {
dateFormat="P" setValue("secretPath", "");
popUpProps={{ setValue("secretKey", "");
open: isEndDatePickerOpen, }
onOpenChange: setIsEndDatePickerOpen resetField("environment");
onChange(e);
}}
placeholder="All projects"
options={workspacesInOrg.map(({ name, id, type }) => ({
name,
id,
type
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Environment"
hoverTooltip={
!selectedProject
? "Select a project before filtering by environment."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
onClear={() => {
resetField("environment");
}} }}
popUpContentProps={{}} >
/> <Controller
</FormControl> control={control}
); name="environment"
}} render={({ field: { onChange, value }, fieldState: { error } }) => (
/> <FormControl
<Button errorText={error?.message}
isLoading={false} isError={Boolean(error)}
colorSchema="primary" className="w-full"
variant="outline_bg" >
className="mt-[0.45rem]" <FilterableSelect
type="submit" value={value}
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />} key={value?.name || "filter-environment"}
onClick={() => isClearable
reset({ isDisabled={!selectedProject}
eventType: presets?.eventType || [], onChange={(e) => onChange(e)}
actor: presets?.actorId, placeholder="All environments"
userAgentType: undefined, options={availableEnvironments.map(({ name, slug }) => ({
startDate: undefined, name,
endDate: undefined, slug
project: null, }))}
secretPath: undefined, getOptionValue={(option) => option.slug}
secretKey: undefined getOptionLabel={(option) => option.name}
}) />
} </FormControl>
> )}
Clear filters />
</Button> </LogFilterItem>
<LogFilterItem
label="Secret Path"
hoverTooltip={
!selectedProject
? "Select a project before filtering by secret path."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
onClear={() => {
setValue("secretPath", "");
}}
>
<Controller
control={control}
name="secretPath"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
className="w-full"
>
<Input
placeholder="Enter secret path"
className="disabled:cursor-not-allowed"
isDisabled={!selectedProject}
{...field}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
hoverTooltip={
!selectedProject
? "Select a project before filtering by secret key."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
label="Secret Key"
onClear={() => {
setValue("secretKey", "");
}}
>
<Controller
control={control}
name="secretKey"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to a specific secret."
className="w-full"
>
<Input
isDisabled={!selectedProject}
{...field}
placeholder="Enter secret key"
className="disabled:cursor-not-allowed"
value={value}
onChange={(e) =>
setValue("secretKey", e.target.value, { shouldDirty: true })
}
/>
</FormControl>
)}
/>
</LogFilterItem>
</motion.div>
)}
</AnimatePresence>
</div>
</div> </div>
</div> </DropdownMenuContent>
</div> </DropdownMenu>
); );
}; };
@@ -6,46 +6,40 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks"; import { useDebounce } from "@app/hooks";
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { LogsFilter } from "./LogsFilter"; import { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable"; import { LogsTable } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types"; import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types";
type Props = { type Props = {
presets?: { presets?: Presets;
actorId?: string;
eventType?: EventType[];
actorType?: ActorType;
startDate?: Date;
endDate?: Date;
eventMetadata?: Record<string, string>;
};
showFilters?: boolean;
filterClassName?: string;
refetchInterval?: number; refetchInterval?: number;
showFilters?: boolean;
}; };
export const LogsSection = withPermission( export const LogsSection = withPermission(
({ presets, filterClassName, refetchInterval, showFilters }: Props) => { ({ presets, refetchInterval, showFilters = true }: Props) => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const { control, reset, watch, setValue } = useForm<AuditLogFilterFormData>({ const { control, reset, watch, getFieldState, resetField, setValue } =
resolver: zodResolver(auditLogFilterFormSchema), useForm<AuditLogFilterFormData>({
defaultValues: { resolver: zodResolver(auditLogFilterFormSchema),
project: null, defaultValues: {
actor: presets?.actorId, project: null,
eventType: presets?.eventType || [], environment: undefined,
page: 1, secretKey: "",
perPage: 10, secretPath: "",
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), // day before today actor: presets?.actorId,
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today eventType: presets?.eventType || [],
} userAgentType: undefined,
}); startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)),
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999))
}
});
useEffect(() => { useEffect(() => {
if (subscription && !subscription.auditLogs) { if (subscription && !subscription.auditLogs) {
@@ -57,6 +51,7 @@ export const LogsSection = withPermission(
const userAgentType = watch("userAgentType") as UserAgentType | undefined; const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor"); const actor = watch("actor");
const projectId = watch("project")?.id; const projectId = watch("project")?.id;
const environment = watch("environment")?.slug;
const secretPath = watch("secretPath"); const secretPath = watch("secretPath");
const secretKey = watch("secretKey"); const secretKey = watch("secretKey");
@@ -67,18 +62,21 @@ export const LogsSection = withPermission(
const [debouncedSecretKey] = useDebounce<string>(secretKey!, 500); const [debouncedSecretKey] = useDebounce<string>(secretKey!, 500);
return ( return (
<div> <div className="space-y-2">
{showFilters && ( <div className="flex w-full justify-end">
<LogsFilter {showFilters && (
isOrgAuditLogs <LogsFilter
className={filterClassName} presets={presets}
presets={presets} control={control}
control={control} watch={watch}
setValue={setValue} reset={reset}
watch={watch} resetField={resetField}
reset={reset} getFieldState={getFieldState}
/> setValue={setValue}
)} />
)}
</div>
<LogsTable <LogsTable
refetchInterval={refetchInterval} refetchInterval={refetchInterval}
filter={{ filter={{
@@ -92,6 +90,7 @@ export const LogsSection = withPermission(
userAgentType, userAgentType,
startDate, startDate,
endDate, endDate,
environment,
actor actor
}} }}
/> />
@@ -1,10 +1,12 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { import {
Button, Button,
EmptyState, EmptyState,
Spinner,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -52,7 +54,9 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th className="w-24" /> <Th className="w-24">
<Spinner size="xs" className={twMerge(isPending ? "opacity-100" : "opacity-0")} />
</Th>
<Th className="w-64"> <Th className="w-64">
Timestamp Timestamp
<Tooltip <Tooltip
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
export const auditLogFilterFormSchema = z export const auditLogFilterFormSchema = z
@@ -10,6 +10,7 @@ export const auditLogFilterFormSchema = z
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) }) .object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
.optional() .optional()
.nullable(), .nullable(),
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
eventType: z.nativeEnum(EventType).array(), eventType: z.nativeEnum(EventType).array(),
actor: z.string().optional(), actor: z.string().optional(),
userAgentType: z.nativeEnum(UserAgentType), userAgentType: z.nativeEnum(UserAgentType),
@@ -40,3 +41,12 @@ export type SetValueType = (
shouldDirty?: boolean; shouldDirty?: boolean;
} }
) => void; ) => void;
export type Presets = {
actorId?: string;
eventType?: EventType[];
actorType?: ActorType;
startDate?: Date;
endDate?: Date;
eventMetadata?: Record<string, string>;
};
@@ -1,8 +1,3 @@
import { useState } from "react";
import { faFilter } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { OrgUser } from "@app/hooks/api/types"; import { OrgUser } from "@app/hooks/api/types";
@@ -14,7 +9,6 @@ type Props = {
export const UserAuditLogsSection = withPermission( export const UserAuditLogsSection = withPermission(
({ orgMembership }: Props) => { ({ orgMembership }: Props) => {
const [showFilter, setShowFilter] = useState(false);
const { subscription } = useSubscription(); const { subscription } = useSubscription();
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
@@ -23,25 +17,8 @@ export const UserAuditLogsSection = withPermission(
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Audit Logs</p> <p className="text-lg font-semibold text-gray-200">Audit Logs</p>
<Tooltip content="Show audit log filters">
<IconButton
colorSchema="primary"
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => setShowFilter(!showFilter)}
>
<div className="flex items-center space-x-2">
<p>Filter</p>
<FontAwesomeIcon icon={faFilter} />
</div>
</IconButton>
</Tooltip>
</div> </div>
<LogsSection <LogsSection
showFilters={showFilter}
filterClassName="bg-mineshaft-900 static"
presets={{ presets={{
actorId: orgMembership.user.id actorId: orgMembership.user.id
}} }}
@@ -35,7 +35,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)), startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
eventType: INTEGRATION_EVENTS eventType: INTEGRATION_EVENTS
}} }}
filterClassName="bg-mineshaft-900 static"
/> />
</div> </div>
) : ( ) : (
@@ -41,7 +41,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)), startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
eventType: INTEGRATION_EVENTS eventType: INTEGRATION_EVENTS
}} }}
filterClassName="bg-mineshaft-900 static"
/> />
) : ( ) : (
<div className="flex h-full items-center justify-center rounded-lg bg-mineshaft-800 text-sm text-mineshaft-200"> <div className="flex h-full items-center justify-center rounded-lg bg-mineshaft-800 text-sm text-mineshaft-200">