Merge 64bcb16ba3 into 09be5d45d5
This commit is contained in:
commit
b99d97e00b
@ -2861,6 +2861,14 @@ Trusted proxies:
|
|||||||
- When a connection comes from one of these IPs, OpenClaw uses `x-forwarded-for` (or `x-real-ip`) to determine the client IP for local pairing checks and HTTP auth/local checks.
|
- When a connection comes from one of these IPs, OpenClaw uses `x-forwarded-for` (or `x-real-ip`) to determine the client IP for local pairing checks and HTTP auth/local checks.
|
||||||
- Only list proxies you fully control, and ensure they **overwrite** incoming `x-forwarded-for`.
|
- Only list proxies you fully control, and ensure they **overwrite** incoming `x-forwarded-for`.
|
||||||
|
|
||||||
|
Device auto-approve:
|
||||||
|
- `gateway.devices.autoApprove` controls whether new device pairing requests are auto-approved.
|
||||||
|
- Valid values: `"none"` (default) or `"tailscale"`.
|
||||||
|
- `"none"`: only local (loopback) connections are auto-approved; remote connections require manual approval.
|
||||||
|
- `"tailscale"`: auto-approve when the connection authenticates via Tailscale Serve identity (verified via `tailscale whois`).
|
||||||
|
- This is useful for personal tailnets where you trust all machines on your network.
|
||||||
|
- For shared or corporate tailnets, consider keeping `"none"` and approving devices manually.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `openclaw gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
- `openclaw gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
||||||
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
|
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
|
||||||
|
|||||||
83
src/config/config.gateway-devices-autoapprove.test.ts
Normal file
83
src/config/config.gateway-devices-autoapprove.test.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { MoltbotSchema } from "./zod-schema.js";
|
||||||
|
|
||||||
|
describe("gateway.devices.autoApprove config", () => {
|
||||||
|
it("accepts valid autoApprove=none", () => {
|
||||||
|
const result = MoltbotSchema.safeParse({
|
||||||
|
gateway: {
|
||||||
|
devices: {
|
||||||
|
autoApprove: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.gateway?.devices?.autoApprove).toBe("none");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts valid autoApprove=tailscale", () => {
|
||||||
|
const result = MoltbotSchema.safeParse({
|
||||||
|
gateway: {
|
||||||
|
devices: {
|
||||||
|
autoApprove: "tailscale",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.gateway?.devices?.autoApprove).toBe("tailscale");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts omitted autoApprove (defaults to none)", () => {
|
||||||
|
const result = MoltbotSchema.safeParse({
|
||||||
|
gateway: {
|
||||||
|
devices: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.gateway?.devices?.autoApprove).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts omitted devices config entirely", () => {
|
||||||
|
const result = MoltbotSchema.safeParse({
|
||||||
|
gateway: {
|
||||||
|
port: 18789,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.gateway?.devices).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid autoApprove values", () => {
|
||||||
|
const invalidValues = ["all", "always", "local", "open", "", "TAILSCALE", "Tailscale"];
|
||||||
|
for (const value of invalidValues) {
|
||||||
|
const result = MoltbotSchema.safeParse({
|
||||||
|
gateway: {
|
||||||
|
devices: {
|
||||||
|
autoApprove: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.success, `should reject autoApprove=${value}`).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects extra properties in devices config", () => {
|
||||||
|
const result = MoltbotSchema.safeParse({
|
||||||
|
gateway: {
|
||||||
|
devices: {
|
||||||
|
autoApprove: "none",
|
||||||
|
unknownProperty: "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -102,4 +102,12 @@ describe("config schema", () => {
|
|||||||
expect(defaultsHint?.help).toContain("last");
|
expect(defaultsHint?.help).toContain("last");
|
||||||
expect(listHint?.help).toContain("bluebubbles");
|
expect(listHint?.help).toContain("bluebubbles");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes gateway.devices.autoApprove ui hints", () => {
|
||||||
|
const res = buildConfigSchema();
|
||||||
|
const autoApproveHint = res.uiHints["gateway.devices.autoApprove"];
|
||||||
|
expect(autoApproveHint?.label).toBe("Device Auto-Approve Policy");
|
||||||
|
expect(autoApproveHint?.help).toContain("none");
|
||||||
|
expect(autoApproveHint?.help).toContain("tailscale");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -209,6 +209,7 @@ const FIELD_LABELS: Record<string, string> = {
|
|||||||
"gateway.nodes.browser.node": "Gateway Node Browser Pin",
|
"gateway.nodes.browser.node": "Gateway Node Browser Pin",
|
||||||
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
|
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
|
||||||
"gateway.nodes.denyCommands": "Gateway Node Denylist",
|
"gateway.nodes.denyCommands": "Gateway Node Denylist",
|
||||||
|
"gateway.devices.autoApprove": "Device Auto-Approve Policy",
|
||||||
"nodeHost.browserProxy.enabled": "Node Browser Proxy Enabled",
|
"nodeHost.browserProxy.enabled": "Node Browser Proxy Enabled",
|
||||||
"nodeHost.browserProxy.allowProfiles": "Node Browser Proxy Allowed Profiles",
|
"nodeHost.browserProxy.allowProfiles": "Node Browser Proxy Allowed Profiles",
|
||||||
"skills.load.watch": "Watch Skills",
|
"skills.load.watch": "Watch Skills",
|
||||||
@ -399,6 +400,8 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
|
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
|
||||||
"gateway.nodes.denyCommands":
|
"gateway.nodes.denyCommands":
|
||||||
"Commands to block even if present in node claims or default allowlist.",
|
"Commands to block even if present in node claims or default allowlist.",
|
||||||
|
"gateway.devices.autoApprove":
|
||||||
|
'Auto-approve policy for new device pairing requests ("none" or "tailscale").',
|
||||||
"nodeHost.browserProxy.enabled": "Expose the local browser control server via node proxy.",
|
"nodeHost.browserProxy.enabled": "Expose the local browser control server via node proxy.",
|
||||||
"nodeHost.browserProxy.allowProfiles":
|
"nodeHost.browserProxy.allowProfiles":
|
||||||
"Optional allowlist of browser profile names exposed via the node proxy.",
|
"Optional allowlist of browser profile names exposed via the node proxy.",
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
export type GatewayBindMode = "auto" | "lan" | "loopback" | "custom" | "tailnet";
|
export type GatewayBindMode = "auto" | "lan" | "loopback" | "custom" | "tailnet";
|
||||||
|
|
||||||
|
export type GatewayDeviceAutoApproveMode = "none" | "tailscale";
|
||||||
|
|
||||||
|
export type GatewayDevicesConfig = {
|
||||||
|
/** Auto-approve policy for new device pairing requests. */
|
||||||
|
autoApprove?: GatewayDeviceAutoApproveMode;
|
||||||
|
};
|
||||||
|
|
||||||
export type GatewayTlsConfig = {
|
export type GatewayTlsConfig = {
|
||||||
/** Enable TLS for the gateway server. */
|
/** Enable TLS for the gateway server. */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@ -235,6 +242,8 @@ export type GatewayConfig = {
|
|||||||
tls?: GatewayTlsConfig;
|
tls?: GatewayTlsConfig;
|
||||||
http?: GatewayHttpConfig;
|
http?: GatewayHttpConfig;
|
||||||
nodes?: GatewayNodesConfig;
|
nodes?: GatewayNodesConfig;
|
||||||
|
/** Device pairing settings. */
|
||||||
|
devices?: GatewayDevicesConfig;
|
||||||
/**
|
/**
|
||||||
* IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection
|
* IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection
|
||||||
* arrives from one of these IPs, the Gateway trusts `x-forwarded-for` (or
|
* arrives from one of these IPs, the Gateway trusts `x-forwarded-for` (or
|
||||||
|
|||||||
@ -443,6 +443,12 @@ export const OpenClawSchema = z
|
|||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
devices: z
|
||||||
|
.object({
|
||||||
|
autoApprove: z.enum(["none", "tailscale"]).optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@ -84,6 +84,8 @@ export const deviceHandlers: GatewayRequestHandlers = {
|
|||||||
deviceId: approved.device.deviceId,
|
deviceId: approved.device.deviceId,
|
||||||
decision: "approved",
|
decision: "approved",
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
|
autoApproved: false,
|
||||||
|
autoApproveReason: null,
|
||||||
},
|
},
|
||||||
{ dropIfSlow: true },
|
{ dropIfSlow: true },
|
||||||
);
|
);
|
||||||
@ -116,6 +118,8 @@ export const deviceHandlers: GatewayRequestHandlers = {
|
|||||||
deviceId: rejected.deviceId,
|
deviceId: rejected.deviceId,
|
||||||
decision: "rejected",
|
decision: "rejected",
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
|
autoApproved: false,
|
||||||
|
autoApproveReason: null,
|
||||||
},
|
},
|
||||||
{ dropIfSlow: true },
|
{ dropIfSlow: true },
|
||||||
);
|
);
|
||||||
|
|||||||
152
src/gateway/server/ws-connection/device-auto-approve.test.ts
Normal file
152
src/gateway/server/ws-connection/device-auto-approve.test.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import type { GatewayDeviceAutoApproveMode } from "../../../config/types.gateway.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replicates the shouldAutoApprove logic from message-handler.ts for testing.
|
||||||
|
* This must match the implementation in message-handler.ts exactly.
|
||||||
|
*/
|
||||||
|
function computeShouldAutoApprove(params: {
|
||||||
|
isLocalClient: boolean;
|
||||||
|
deviceAutoApprove: GatewayDeviceAutoApproveMode;
|
||||||
|
authMethod: string;
|
||||||
|
}): boolean {
|
||||||
|
const { isLocalClient, deviceAutoApprove, authMethod } = params;
|
||||||
|
return isLocalClient || (deviceAutoApprove === "tailscale" && authMethod === "tailscale");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("device auto-approve logic", () => {
|
||||||
|
describe("local client", () => {
|
||||||
|
it("auto-approves local clients regardless of config", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: true,
|
||||||
|
deviceAutoApprove: "none",
|
||||||
|
authMethod: "token",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("auto-approves local clients even with tailscale config", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: true,
|
||||||
|
deviceAutoApprove: "tailscale",
|
||||||
|
authMethod: "token",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("config=none (default)", () => {
|
||||||
|
it("does NOT auto-approve remote clients with token auth", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "none",
|
||||||
|
authMethod: "token",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT auto-approve remote clients with password auth", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "none",
|
||||||
|
authMethod: "password",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT auto-approve remote clients with tailscale auth when config is none", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "none",
|
||||||
|
authMethod: "tailscale",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT auto-approve remote clients with device-token auth", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "none",
|
||||||
|
authMethod: "device-token",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("config=tailscale", () => {
|
||||||
|
it("auto-approves remote clients with tailscale auth", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "tailscale",
|
||||||
|
authMethod: "tailscale",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT auto-approve remote clients with token auth", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "tailscale",
|
||||||
|
authMethod: "token",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT auto-approve remote clients with password auth", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "tailscale",
|
||||||
|
authMethod: "password",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT auto-approve remote clients with device-token auth", () => {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "tailscale",
|
||||||
|
authMethod: "device-token",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("security invariants", () => {
|
||||||
|
it("never auto-approves non-tailscale remote auth when config is tailscale", () => {
|
||||||
|
const nonTailscaleAuthMethods = ["token", "password", "device-token", "unknown", ""];
|
||||||
|
for (const authMethod of nonTailscaleAuthMethods) {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "tailscale",
|
||||||
|
authMethod,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never auto-approves any remote auth when config is none", () => {
|
||||||
|
const allAuthMethods = ["token", "password", "device-token", "tailscale", "unknown", ""];
|
||||||
|
for (const authMethod of allAuthMethods) {
|
||||||
|
expect(
|
||||||
|
computeShouldAutoApprove({
|
||||||
|
isLocalClient: false,
|
||||||
|
deviceAutoApprove: "none",
|
||||||
|
authMethod,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -623,6 +623,10 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
|
|
||||||
const skipPairing = allowControlUiBypass && hasSharedAuth;
|
const skipPairing = allowControlUiBypass && hasSharedAuth;
|
||||||
if (device && devicePublicKey && !skipPairing) {
|
if (device && devicePublicKey && !skipPairing) {
|
||||||
|
const deviceAutoApprove = configSnapshot.gateway?.devices?.autoApprove ?? "none";
|
||||||
|
const shouldAutoApprove =
|
||||||
|
isLocalClient || (deviceAutoApprove === "tailscale" && authMethod === "tailscale");
|
||||||
|
|
||||||
const requirePairing = async (reason: string, _paired?: { deviceId: string }) => {
|
const requirePairing = async (reason: string, _paired?: { deviceId: string }) => {
|
||||||
const pairing = await requestDevicePairing({
|
const pairing = await requestDevicePairing({
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
@ -634,7 +638,7 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
role,
|
role,
|
||||||
scopes,
|
scopes,
|
||||||
remoteIp: reportedClientIp,
|
remoteIp: reportedClientIp,
|
||||||
silent: isLocalClient,
|
silent: shouldAutoApprove,
|
||||||
});
|
});
|
||||||
const context = buildRequestContext();
|
const context = buildRequestContext();
|
||||||
if (pairing.request.silent === true) {
|
if (pairing.request.silent === true) {
|
||||||
@ -650,6 +654,8 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
deviceId: approved.device.deviceId,
|
deviceId: approved.device.deviceId,
|
||||||
decision: "approved",
|
decision: "approved",
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
|
autoApproved: true,
|
||||||
|
autoApproveReason: isLocalClient ? "local" : "tailscale",
|
||||||
},
|
},
|
||||||
{ dropIfSlow: true },
|
{ dropIfSlow: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@ -104,6 +104,55 @@ describe("security audit", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits info when device auto-approve is set to tailscale", async () => {
|
||||||
|
const cfg: MoltbotConfig = {
|
||||||
|
gateway: {
|
||||||
|
devices: {
|
||||||
|
autoApprove: "tailscale",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await runSecurityAudit({
|
||||||
|
config: cfg,
|
||||||
|
includeFilesystem: false,
|
||||||
|
includeChannelSecurity: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.findings).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
checkId: "gateway.devices.auto_approve_tailscale",
|
||||||
|
severity: "info",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT emit device auto-approve finding when set to none", async () => {
|
||||||
|
const cfg: MoltbotConfig = {
|
||||||
|
gateway: {
|
||||||
|
devices: {
|
||||||
|
autoApprove: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await runSecurityAudit({
|
||||||
|
config: cfg,
|
||||||
|
includeFilesystem: false,
|
||||||
|
includeChannelSecurity: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.findings).not.toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
checkId: "gateway.devices.auto_approve_tailscale",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("flags logging.redactSensitive=off", async () => {
|
it("flags logging.redactSensitive=off", async () => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
logging: { redactSensitive: "off" },
|
logging: { redactSensitive: "off" },
|
||||||
|
|||||||
@ -320,6 +320,18 @@ function collectGatewayConfigFindings(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deviceAutoApprove = cfg.gateway?.devices?.autoApprove ?? "none";
|
||||||
|
if (deviceAutoApprove === "tailscale") {
|
||||||
|
findings.push({
|
||||||
|
checkId: "gateway.devices.auto_approve_tailscale",
|
||||||
|
severity: "info",
|
||||||
|
title: "Device auto-approve via Tailscale enabled",
|
||||||
|
detail:
|
||||||
|
'gateway.devices.autoApprove="tailscale" auto-approves device pairing for Tailscale-authenticated connections. ' +
|
||||||
|
'Safe for personal tailnets; for shared/corporate tailnets, consider keeping autoApprove="none".',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (cfg.gateway?.controlUi?.allowInsecureAuth === true) {
|
if (cfg.gateway?.controlUi?.allowInsecureAuth === true) {
|
||||||
findings.push({
|
findings.push({
|
||||||
checkId: "gateway.control_ui.insecure_auth",
|
checkId: "gateway.control_ui.insecure_auth",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user