170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { jsonValueSchema } from "./jsonValue.js";
|
|
|
|
/** Supported transport types for custom protocol capture. */
|
|
export const transportTypeSchema = z.enum([
|
|
"tcp",
|
|
"udp",
|
|
"ipc",
|
|
"unix-socket",
|
|
"named-pipe",
|
|
"xpc",
|
|
]);
|
|
export type TransportType = z.infer<typeof transportTypeSchema>;
|
|
|
|
/** Direction of a protocol frame. */
|
|
export const frameDirectionSchema = z.enum(["sent", "received", "intercepted"]);
|
|
|
|
/** A captured protocol frame. */
|
|
export const protocolFrameSchema = z.strictObject({
|
|
/** Monotonic sequence number. */
|
|
sequence: z.number().int().nonnegative(),
|
|
/** Timestamp in milliseconds. */
|
|
at_ms: z.number().int().nonnegative(),
|
|
/** Transport type. */
|
|
transport: transportTypeSchema,
|
|
/** Direction of the frame. */
|
|
direction: frameDirectionSchema,
|
|
/** Source process identity. */
|
|
source_pid: z.number().int().nullable(),
|
|
/** Destination process identity. */
|
|
dest_pid: z.number().int().nullable(),
|
|
/** Endpoint address (IP:port, socket path, pipe name). */
|
|
source_endpoint: z.string().nullable(),
|
|
/** Destination endpoint. */
|
|
dest_endpoint: z.string().nullable(),
|
|
/** Raw frame data as base64. */
|
|
raw_data: z.string().nullable(),
|
|
/** Decoded/parsed frame content if known. */
|
|
decoded_content: jsonValueSchema.nullable(),
|
|
/** Frame size in bytes. */
|
|
size: z.number().int().nonnegative(),
|
|
/** Whether the frame was truncated. */
|
|
truncated: z.boolean().default(false),
|
|
});
|
|
export type ProtocolFrame = z.infer<typeof protocolFrameSchema>;
|
|
|
|
/** Authentication flow stage. */
|
|
export const authStageSchema = z.enum([
|
|
"init",
|
|
"challenge",
|
|
"response",
|
|
"token_exchange",
|
|
"renewal",
|
|
"revocation",
|
|
"failure",
|
|
"success",
|
|
]);
|
|
|
|
/** Token lifecycle event. */
|
|
export const tokenLifecycleEventSchema = z.enum([
|
|
"issued",
|
|
"refreshed",
|
|
"expired",
|
|
"revoked",
|
|
"renewed",
|
|
]);
|
|
|
|
/** A captured authentication flow event. */
|
|
export const authFlowEventSchema = z.strictObject({
|
|
/** Monotonic sequence number. */
|
|
sequence: z.number().int().nonnegative(),
|
|
/** Timestamp in milliseconds. */
|
|
at_ms: z.number().int().nonnegative(),
|
|
/** Authentication stage. */
|
|
stage: authStageSchema,
|
|
/** Protocol used. */
|
|
protocol: z.string().min(1),
|
|
/** Token type if applicable. */
|
|
token_type: z.string().nullable(),
|
|
/** Token lifecycle event. */
|
|
token_lifecycle: tokenLifecycleEventSchema.nullable(),
|
|
/** Correlation ID linking challenge-response pairs. */
|
|
correlation_id: z.string().nullable(),
|
|
/** Whether the capture producer marked credentials as redacted. */
|
|
credentials_redacted: z.boolean().default(false),
|
|
/** Whether the authentication succeeded. */
|
|
succeeded: z.boolean().default(false),
|
|
/** Error message if authentication failed. */
|
|
error: z.string().nullable(),
|
|
});
|
|
export type AuthFlowEvent = z.infer<typeof authFlowEventSchema>;
|
|
|
|
/** A captured protocol session. */
|
|
export const customProtocolCaptureSchema = z.strictObject({
|
|
/** Transport type for this session. */
|
|
transport: transportTypeSchema,
|
|
/** Captured frames in order. */
|
|
frames: z.array(protocolFrameSchema),
|
|
/** Authentication flow events. */
|
|
auth_events: z.array(authFlowEventSchema).default([]),
|
|
/** Whether any frame was truncated. */
|
|
has_truncated: z.boolean().default(false),
|
|
/** Whether the capture producer detected credential content. */
|
|
credentials_detected: z.boolean().default(false),
|
|
});
|
|
|
|
/** Correlate a frame with process identity. */
|
|
export function correlateProcessIdentity(
|
|
frame: ProtocolFrame,
|
|
pid: number,
|
|
): boolean {
|
|
return frame.source_pid === pid || frame.dest_pid === pid;
|
|
}
|
|
|
|
/** Check if a frame contains credential-like content. */
|
|
export function looksLikeCredentialFrame(frame: ProtocolFrame): boolean {
|
|
const patterns = [
|
|
"password",
|
|
"token",
|
|
"secret",
|
|
"api_key",
|
|
"apikey",
|
|
"authorization",
|
|
"credential",
|
|
];
|
|
const content = JSON.stringify(frame.decoded_content ?? "").toLowerCase();
|
|
return patterns.some((p) => content.includes(p));
|
|
}
|
|
|
|
/** Filter frames by transport type. */
|
|
export function framesByTransport(
|
|
frames: readonly ProtocolFrame[],
|
|
transport: TransportType,
|
|
): ProtocolFrame[] {
|
|
return frames.filter((f) => f.transport === transport);
|
|
}
|
|
|
|
/** Get all authentication flow events for a specific protocol. */
|
|
export function authEventsByProtocol(
|
|
events: readonly AuthFlowEvent[],
|
|
protocol: string,
|
|
): AuthFlowEvent[] {
|
|
return events.filter((e) => e.protocol === protocol);
|
|
}
|
|
|
|
/** Get successful authentication events. */
|
|
export function successfulAuthEvents(
|
|
events: readonly AuthFlowEvent[],
|
|
): AuthFlowEvent[] {
|
|
return events.filter((e) => e.succeeded);
|
|
}
|
|
|
|
/** Get failed authentication events. */
|
|
export function failedAuthEvents(
|
|
events: readonly AuthFlowEvent[],
|
|
): AuthFlowEvent[] {
|
|
return events.filter((e) => !e.succeeded);
|
|
}
|
|
|
|
/** Compute the duration of an authentication flow in milliseconds. */
|
|
export function authFlowDuration(
|
|
events: readonly AuthFlowEvent[],
|
|
): number | null {
|
|
const first = events.at(0);
|
|
const last = events.at(-1);
|
|
if (first === undefined || last === undefined) return null;
|
|
return last.at_ms - first.at_ms;
|
|
}
|