

























@@ -4,6 +4,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
44import type { WebSocket } from "ws";
55import { PROTOCOL_VERSION } from "../../../../packages/gateway-protocol/src/index.js";
66import type { HealthSummary } from "../../../commands/health.types.js";
7+import {
8+onInternalDiagnosticEvent,
9+resetDiagnosticEventsForTest,
10+type DiagnosticSecurityEvent,
11+} from "../../../infra/diagnostic-events.js";
712import type { ResolvedGatewayAuth } from "../../auth.js";
813import { getOperatorApprovalRuntimeToken } from "../../operator-approval-runtime-token.js";
914import { handleGatewayRequest } from "../../server-methods.js";
@@ -153,6 +158,19 @@ function createSetCloseCauseMock() {
153158return vi.fn<SetCloseCause>();
154159}
155160161+function captureSecurityEvents(): {
162+events: DiagnosticSecurityEvent[];
163+stop: () => void;
164+} {
165+const events: DiagnosticSecurityEvent[] = [];
166+const stop = onInternalDiagnosticEvent((event, metadata) => {
167+if (metadata.trusted && event.type === "security.event") {
168+events.push(event);
169+}
170+});
171+return { events, stop };
172+}
173+156174function attachGatewayHarness(options: {
157175connId: string;
158176connectNonce: string;
@@ -263,6 +281,7 @@ function attachGatewayHarness(options: {
263281264282describe("attachGatewayWsMessageHandler post-connect health refresh", () => {
265283beforeEach(() => {
284+resetDiagnosticEventsForTest();
266285vi.clearAllMocks();
267286});
268287@@ -403,32 +422,122 @@ describe("attachGatewayWsMessageHandler post-connect health refresh", () => {
403422 refreshHealthSnapshot,
404423 isClosed,
405424});
425+const captured = captureSecurityEvents();
426+427+try {
428+harness.sendConnect("connect-1", {
429+minProtocol: PROTOCOL_VERSION,
430+maxProtocol: PROTOCOL_VERSION,
431+client: {
432+id: "openclaw-control-ui",
433+version: "dev",
434+platform: "test",
435+mode: "ui",
436+},
437+role: "operator",
438+caps: [],
439+});
406440407-harness.sendConnect("connect-1", {
408-minProtocol: PROTOCOL_VERSION,
409-maxProtocol: PROTOCOL_VERSION,
410-client: {
411-id: "openclaw-control-ui",
412-version: "dev",
413-platform: "test",
414-mode: "ui",
415-},
416-role: "operator",
417-caps: [],
418-});
419-420-await vi.waitFor(() => {
421-expect(harness.socketSend).toHaveBeenCalled();
422-});
441+await vi.waitFor(() => {
442+expect(harness.socketSend).toHaveBeenCalled();
443+});
444+} finally {
445+captured.stop();
446+}
423447const hello = JSON.parse(harness.socketSend.mock.calls.at(0)?.[0] ?? "{}") as { ok?: boolean };
424448expect(hello.ok).toBe(true);
449+expect(captured.events).toHaveLength(1);
450+expect(captured.events[0]).toMatchObject({
451+action: "gateway.auth.succeeded",
452+outcome: "success",
453+severity: "low",
454+actor: { kind: "operator", role: "operator" },
455+target: { kind: "gateway", name: "websocket" },
456+policy: { id: "gateway.websocket-auth", decision: "allow" },
457+control: { id: "gateway.ws.connect", family: "auth" },
458+attributes: {
459+auth_mode: "none",
460+auth_method: "none",
461+auth_provided: "none",
462+client_mode: "ui",
463+has_device_identity: false,
464+scope_count: 0,
465+},
466+});
425467426468await vi.waitFor(() => {
427469expect(refreshHealthSnapshot).toHaveBeenCalledWith({ probe: false });
428470});
429471resolveRefresh?.();
430472});
431473474+it("emits a security event for rejected gateway auth", async () => {
475+const close = createCloseMock();
476+const harness = attachGatewayHarness({
477+connId: "conn-auth-failed",
478+connectNonce: "nonce-auth-failed",
479+requestHost: "gateway.example.com:18789",
480+remoteAddr: "203.0.113.50",
481+resolvedAuth: {
482+mode: "token",
483+token: "gateway-token",
484+allowTailscale: false,
485+},
486+ close,
487+});
488+const captured = captureSecurityEvents();
489+490+try {
491+harness.sendConnect("connect-auth-failed", {
492+minProtocol: PROTOCOL_VERSION,
493+maxProtocol: PROTOCOL_VERSION,
494+client: {
495+id: "gateway-client",
496+version: "dev",
497+platform: "test",
498+mode: "backend",
499+},
500+role: "operator",
501+scopes: ["operator.admin"],
502+caps: [],
503+auth: { token: "wrong-token" },
504+});
505+506+await vi.waitFor(() => {
507+expect(close).toHaveBeenCalledWith(1008, expect.stringContaining("unauthorized"));
508+});
509+} finally {
510+captured.stop();
511+}
512+513+expect(captured.events).toHaveLength(1);
514+expect(captured.events[0]).toMatchObject({
515+action: "gateway.auth.failed",
516+outcome: "denied",
517+severity: "medium",
518+reason: "token_mismatch",
519+actor: { kind: "operator", role: "operator" },
520+target: { kind: "gateway", name: "websocket" },
521+policy: {
522+id: "gateway.websocket-auth",
523+decision: "deny",
524+reason: "token_mismatch",
525+},
526+control: { id: "gateway.ws.connect", family: "auth" },
527+attributes: {
528+auth_mode: "token",
529+auth_method: "token",
530+auth_provided: "token",
531+client_mode: "backend",
532+has_device_identity: false,
533+scope_count: 0,
534+rate_limited: false,
535+},
536+});
537+expect(JSON.stringify(captured.events)).not.toContain("wrong-token");
538+expect(JSON.stringify(captured.events)).not.toContain("gateway-token");
539+});
540+432541it("does not mark local backend self-pairing clients as approval runtimes", async () => {
433542const refreshHealthSnapshot = vi.fn<GatewayRequestContext["refreshHealthSnapshot"]>(async () =>
434543createHealthSummary(),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。