diff --git a/backend/src/ee/services/pam-session/pam-session-schemas.ts b/backend/src/ee/services/pam-session/pam-session-schemas.ts index 328509739..f0fe5702c 100644 --- a/backend/src/ee/services/pam-session/pam-session-schemas.ts +++ b/backend/src/ee/services/pam-session/pam-session-schemas.ts @@ -30,16 +30,14 @@ export const HttpBaseEventSchema = z.object({ export const HttpRequestEventSchema = HttpBaseEventSchema.extend({ eventType: z.literal(HttpEventTypeSchema.Values.request), method: z.string(), - url: z.string(), + url: z.string() // TODO: optional for now - body: z.string().optional() }); export const HttpResponseEventSchema = HttpBaseEventSchema.extend({ eventType: z.literal(HttpEventTypeSchema.Values.response), - status: z.string(), + status: z.string() // TODO: optional for now - body: z.string().optional() }); export const HttpEventSchema = z.discriminatedUnion("eventType", [HttpRequestEventSchema, HttpResponseEventSchema]); diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index bff8ac25e..56f66c6f1 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -44,7 +44,26 @@ export type TTerminalEvent = { elapsedTime: number; // Seconds since session start (for replay) }; -export type TPamSessionLog = TPamCommandLog | TTerminalEvent; +export type THttpRequestEvent = { + timestamp: string; + requestId: string; + eventType: "request"; + headers: Record; + method: string; + url: string; +}; + +export type THttpResponseEvent = { + timestamp: string; + requestId: string; + eventType: "response"; + headers: Record; + status: string; +}; + +export type THttpEvent = THttpRequestEvent | THttpResponseEvent; + +export type TPamSessionLog = TPamCommandLog | TTerminalEvent | THttpEvent; export type TPamSession = { id: string; diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/HttpEventView.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/HttpEventView.tsx new file mode 100644 index 000000000..9ab228942 --- /dev/null +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/HttpEventView.tsx @@ -0,0 +1,138 @@ +import { useMemo, useState } from "react"; +import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Input } from "@app/components/v2"; +import { HighlightText } from "@app/components/v2/HighlightText"; +import { THttpEvent } from "@app/hooks/api/pam"; + +type Props = { + events: THttpEvent[]; +}; + +export const HttpEventView = ({ events }: Props) => { + const [search, setSearch] = useState(""); + + const filteredEvents = useMemo( + () => + events.filter((event) => { + const searchValue = search.trim().toLowerCase(); + if (!searchValue) return true; + + if (event.eventType === "request") { + return ( + event.method.toLowerCase().includes(searchValue) || + event.url.toLowerCase().includes(searchValue) || + event.requestId.toLowerCase().includes(searchValue) || + Object.keys(event.headers).some((key) => key.toLowerCase().includes(searchValue)) || + Object.values(event.headers).some((values) => + values.some((value) => value.toLowerCase().includes(searchValue)) + ) + ); + } + return ( + event.status.toLowerCase().includes(searchValue) || + event.requestId.toLowerCase().includes(searchValue) || + Object.keys(event.headers).some((key) => key.toLowerCase().includes(searchValue)) || + Object.values(event.headers).some((values) => + values.some((value) => value.toLowerCase().includes(searchValue)) + ) + ); + }), + [events, search] + ); + + const formatHeaders = (headers: Record) => { + return Object.entries(headers) + .map(([key, values]) => `${key}: ${values.join(", ")}`) + .join("\n"); + }; + + return ( + <> +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search HTTP events..." + className="flex-1 bg-mineshaft-800" + containerClassName="bg-transparent" + /> +
+ +
+ {filteredEvents.length > 0 ? ( + filteredEvents.map((event, index) => { + const eventKey = `${event.timestamp}-${event.requestId}-${index}`; + const isRequest = event.eventType === "request"; + + return ( +
+
+
+ + {isRequest ? "REQUEST" : "RESPONSE"} + + {new Date(event.timestamp).toLocaleString()} + • + {event.requestId} +
+
+ +
+ {isRequest ? ( +
+ {event.method}{" "} + +
+ ) : ( +
+ Status:{" "} + +
+ )} + + {Object.keys(event.headers).length > 0 && ( +
+
Headers:
+
+ +
+
+ )} +
+
+ ); + }) + ) : ( +
+ {search.length ? ( +
+
No HTTP events match search criteria
+
+ ) : ( +
+
HTTP session logs are not yet available
+
+ Logs will be uploaded after the session duration has elapsed. +
+ If logs do not appear after some time, please contact your Gateway administrators. +
+
+ )} +
+ )} +
+ + ); +}; diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx index 86fd7c1dc..e0c631551 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx +++ b/frontend/src/pages/pam/PamSessionsByIDPage/components/PamSessionLogsSection.tsx @@ -1,6 +1,13 @@ -import { PamResourceType, TPamCommandLog, TPamSession, TTerminalEvent } from "@app/hooks/api/pam"; +import { + PamResourceType, + THttpEvent, + TPamCommandLog, + TPamSession, + TTerminalEvent +} from "@app/hooks/api/pam"; import { CommandLogView } from "./CommandLogView"; +import { HttpEventView } from "./HttpEventView"; import { TerminalEventView } from "./TerminalEventView"; type Props = { @@ -13,6 +20,7 @@ export const PamSessionLogsSection = ({ session }: Props) => { const isDatabaseSession = session.resourceType === PamResourceType.Postgres || session.resourceType === PamResourceType.MySQL; + const isHttpSession = session.resourceType === PamResourceType.Kubernetes; const hasLogs = session.logs.length > 0; return ( @@ -23,6 +31,7 @@ export const PamSessionLogsSection = ({ session }: Props) => { {isDatabaseSession && hasLogs && } {isSSHSession && hasLogs && } + {isHttpSession && hasLogs && } {!hasLogs && (