mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
test(gateway): prove TLS certificate pinning (#118812)
* test(gateway): prove TLS certificate pinning * test(gateway): fix TLS pinning proof types * test(gateway): observe published TLS fingerprint * test(gateway): load TLS discovery probe explicitly * test(gateway): keep QA assertion typed
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
title: Gateway TLS certificate pinning
|
||||
|
||||
scenario:
|
||||
id: gateway-tls-pinning
|
||||
surface: runtime
|
||||
category: gateway.network-access-and-discovery
|
||||
coverage:
|
||||
primary:
|
||||
- gateway.tls-pinning
|
||||
objective: Verify the real Gateway TLS listener and public Node client enforce the advertised certificate fingerprint.
|
||||
successCriteria:
|
||||
- A real loopback Gateway starts with generated TLS certificate material.
|
||||
- The Gateway-advertised SHA-256 fingerprint matches the certificate presented by the live listener.
|
||||
- The public GatewayClient completes hello and a health request with the exact pin.
|
||||
- A wrong pin fails before hello completes.
|
||||
- Supplying a TLS pin to a plaintext Gateway URL fails with an explicit transport-policy error.
|
||||
docsRefs:
|
||||
- docs/gateway/index.md
|
||||
- docs/gateway/discovery.md
|
||||
- docs/gateway/protocol.md
|
||||
codeRefs:
|
||||
- src/infra/tls/gateway.ts
|
||||
- src/gateway/server-runtime-state.ts
|
||||
- packages/gateway-client/src/client.ts
|
||||
- test/e2e/qa-lab/runtime/gateway-tls-pinning.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/gateway-tls-pinning.ts
|
||||
summary: Starts a real TLS Gateway and exercises exact-pin, wrong-pin, and plaintext mismatch behavior through the public GatewayClient.
|
||||
args:
|
||||
- --artifact-base
|
||||
- ${outputDir}
|
||||
@@ -0,0 +1,41 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { runGatewayTlsPinningProducer } from "./gateway-tls-pinning.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { force: true, recursive: true })));
|
||||
});
|
||||
|
||||
describe("Gateway TLS pinning evidence", () => {
|
||||
it("proves the live listener fingerprint and public client pin policy", async () => {
|
||||
const artifactBase = await fs.mkdtemp(path.join(os.tmpdir(), "gateway-tls-pinning-evidence-"));
|
||||
tempDirs.push(artifactBase);
|
||||
|
||||
const evidence = await runGatewayTlsPinningProducer({
|
||||
artifactBase,
|
||||
repoRoot: process.cwd(),
|
||||
});
|
||||
|
||||
expect(evidence.entries[0]?.result.status).toBe("pass");
|
||||
const proof = JSON.parse(
|
||||
await fs.readFile(path.join(artifactBase, "gateway-tls-pinning-summary.json"), "utf8"),
|
||||
) as {
|
||||
advertisedFingerprint: string;
|
||||
cleartextMismatch: string;
|
||||
healthResponded: boolean;
|
||||
peerFingerprint: string;
|
||||
wrongPinFailure: string;
|
||||
wrongPinHelloObserved: boolean;
|
||||
};
|
||||
expect(proof.advertisedFingerprint).toMatch(/^[a-f0-9]{64}$/u);
|
||||
expect(proof.peerFingerprint).toBe(proof.advertisedFingerprint);
|
||||
expect(proof.healthResponded).toBe(true);
|
||||
expect(proof.wrongPinFailure).toMatch(/fingerprint mismatch/iu);
|
||||
expect(proof.wrongPinHelloObserved).toBe(false);
|
||||
expect(proof.cleartextMismatch).toMatch(/fingerprint requires wss:\/\//iu);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,461 @@
|
||||
// QA evidence for the real Gateway TLS listener and public client pinning boundary.
|
||||
import fs from "node:fs/promises";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import tls from "node:tls";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { GatewayClient } from "@openclaw/gateway-client";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import {
|
||||
QA_EVIDENCE_FILENAME,
|
||||
type QaEvidenceSummaryJson,
|
||||
} from "../../../../extensions/qa-lab/api.js";
|
||||
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../../../../src/config/config.js";
|
||||
import { startGatewayServer } from "../../../../src/gateway/server.js";
|
||||
import { formatErrorMessage } from "../../../../src/infra/errors.js";
|
||||
import { normalizeFingerprint } from "../../../../src/infra/tls/fingerprint.js";
|
||||
import { loadGatewayTlsRuntime } from "../../../../src/infra/tls/gateway.js";
|
||||
import { createQaScriptEvidenceWriter } from "./script-evidence.js";
|
||||
|
||||
const SCENARIO_ID = "gateway-tls-pinning";
|
||||
const SOURCE_PATH = "qa/scenarios/runtime/gateway-tls-pinning.yaml";
|
||||
const DISCOVERY_PLUGIN_ID = "tls-discovery-proof";
|
||||
const CONNECTION_TIMEOUT_MS = 15_000;
|
||||
const ENV_KEYS = [
|
||||
"HOME",
|
||||
"NODE_ENV",
|
||||
"OPENCLAW_CONFIG_PATH",
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_BUNDLED_PLUGINS_DIR",
|
||||
"OPENCLAW_DISABLE_BONJOUR",
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
|
||||
"OPENCLAW_TEST_MINIMAL_GATEWAY",
|
||||
"OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR",
|
||||
"OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
|
||||
"OPENCLAW_SKIP_CANVAS_HOST",
|
||||
"OPENCLAW_SKIP_CHANNELS",
|
||||
"OPENCLAW_SKIP_CRON",
|
||||
"OPENCLAW_SKIP_GMAIL_WATCHER",
|
||||
"OPENCLAW_SKIP_PROVIDERS",
|
||||
"VITEST",
|
||||
] as const;
|
||||
|
||||
type ProducerOptions = {
|
||||
artifactBase: string;
|
||||
repoRoot: string;
|
||||
};
|
||||
|
||||
export type GatewayTlsPinningProof = {
|
||||
advertisedFingerprint: string;
|
||||
cleartextMismatch: string;
|
||||
healthResponded: boolean;
|
||||
peerFingerprint: string;
|
||||
wrongPinFailure: string;
|
||||
wrongPinHelloObserved: boolean;
|
||||
};
|
||||
|
||||
function parseOptions(argv: readonly string[]): ProducerOptions {
|
||||
const readValue = (name: string) => {
|
||||
const index = argv.indexOf(name);
|
||||
return index >= 0 ? argv[index + 1] : undefined;
|
||||
};
|
||||
const artifactBase = readValue("--artifact-base");
|
||||
if (!artifactBase) {
|
||||
throw new Error("--artifact-base is required");
|
||||
}
|
||||
return {
|
||||
artifactBase: path.resolve(artifactBase),
|
||||
repoRoot: path.resolve(readValue("--repo-root") ?? process.cwd()),
|
||||
};
|
||||
}
|
||||
|
||||
function captureEnvironment() {
|
||||
const snapshot = new Map<string, string | undefined>(
|
||||
ENV_KEYS.map((key) => [key, process.env[key]]),
|
||||
);
|
||||
return () => {
|
||||
for (const [key, value] of snapshot) {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function writeDiscoveryProbePlugin(
|
||||
pluginDir: string,
|
||||
advertisementPath: string,
|
||||
): Promise<void> {
|
||||
await fs.mkdir(pluginDir, { recursive: true });
|
||||
await Promise.all([
|
||||
fs.writeFile(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
id: DISCOVERY_PLUGIN_ID,
|
||||
activation: { onStartup: true },
|
||||
configSchema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`const fs = require("node:fs");
|
||||
module.exports = {
|
||||
id: ${JSON.stringify(DISCOVERY_PLUGIN_ID)},
|
||||
register(api) {
|
||||
api.registerGatewayDiscoveryService({
|
||||
id: ${JSON.stringify(DISCOVERY_PLUGIN_ID)},
|
||||
advertise(context) {
|
||||
fs.writeFileSync(${JSON.stringify(advertisementPath)}, JSON.stringify(context), "utf8");
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
`,
|
||||
"utf8",
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
async function readAdvertisedFingerprint(advertisementPath: string): Promise<string> {
|
||||
const advertisement: unknown = JSON.parse(await fs.readFile(advertisementPath, "utf8"));
|
||||
if (!isRecord(advertisement) || advertisement.gatewayTlsEnabled !== true) {
|
||||
throw new Error("Gateway discovery publisher did not advertise TLS");
|
||||
}
|
||||
const fingerprint = advertisement.gatewayTlsFingerprintSha256;
|
||||
if (typeof fingerprint !== "string") {
|
||||
throw new Error("Gateway discovery publisher did not advertise a TLS fingerprint");
|
||||
}
|
||||
const normalized = normalizeFingerprint(fingerprint);
|
||||
if (!normalized) {
|
||||
throw new Error("Gateway discovery publisher advertised an invalid TLS fingerprint");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close();
|
||||
throw new Error("failed to allocate a loopback port");
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
return address.port;
|
||||
}
|
||||
|
||||
async function waitForPeerFingerprint(port: number): Promise<string> {
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
const socket = tls.connect({
|
||||
host: "127.0.0.1",
|
||||
minVersion: "TLSv1.3",
|
||||
port,
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
socket.destroy();
|
||||
reject(new Error("timed out reading Gateway TLS peer certificate"));
|
||||
}, CONNECTION_TIMEOUT_MS);
|
||||
timer.unref?.();
|
||||
const cleanup = () => clearTimeout(timer);
|
||||
socket.once("secureConnect", () => {
|
||||
const fingerprint = normalizeFingerprint(socket.getPeerCertificate().fingerprint256 ?? "");
|
||||
cleanup();
|
||||
socket.end();
|
||||
if (!fingerprint) {
|
||||
reject(new Error("Gateway peer certificate did not expose a SHA-256 fingerprint"));
|
||||
return;
|
||||
}
|
||||
resolve(fingerprint);
|
||||
});
|
||||
socket.once("error", (error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, label: string): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error(`${label} timed out after ${CONNECTION_TIMEOUT_MS}ms`)),
|
||||
CONNECTION_TIMEOUT_MS,
|
||||
);
|
||||
timer.unref?.();
|
||||
promise.then(
|
||||
(value) => {
|
||||
clearTimeout(timer);
|
||||
resolve(value);
|
||||
},
|
||||
(error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
return { promise, reject, resolve };
|
||||
}
|
||||
|
||||
async function connectWithExactPin(url: string, tlsFingerprint: string): Promise<boolean> {
|
||||
const hello = createDeferred<void>();
|
||||
const client = new GatewayClient({
|
||||
url,
|
||||
tlsFingerprint,
|
||||
onConnectError: hello.reject,
|
||||
onHelloOk: () => hello.resolve(),
|
||||
});
|
||||
try {
|
||||
client.start();
|
||||
await withTimeout(hello.promise, "Gateway exact-pin hello");
|
||||
const health = await client.request<Record<string, unknown>>("health", {});
|
||||
return health !== null && typeof health === "object";
|
||||
} finally {
|
||||
await client.stopAndWait().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function connectWithWrongPin(
|
||||
url: string,
|
||||
tlsFingerprint: string,
|
||||
): Promise<{ error: string; helloObserved: boolean }> {
|
||||
const failure = createDeferred<Error>();
|
||||
let helloObserved = false;
|
||||
const client = new GatewayClient({
|
||||
url,
|
||||
tlsFingerprint,
|
||||
onConnectError: (error) => {
|
||||
client.stop();
|
||||
failure.resolve(error);
|
||||
},
|
||||
onHelloOk: () => {
|
||||
helloObserved = true;
|
||||
},
|
||||
});
|
||||
try {
|
||||
client.start();
|
||||
const error = await withTimeout(failure.promise, "Gateway wrong-pin failure");
|
||||
if (!/fingerprint mismatch/iu.test(error.message)) {
|
||||
throw new Error(`wrong pin failed for an unexpected reason: ${error.message}`);
|
||||
}
|
||||
if (helloObserved) {
|
||||
throw new Error("wrong TLS pin reached Gateway hello");
|
||||
}
|
||||
return { error: error.message, helloObserved };
|
||||
} finally {
|
||||
await client.stopAndWait().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function proveCleartextMismatch(port: number, tlsFingerprint: string): Promise<string> {
|
||||
const failure = createDeferred<Error>();
|
||||
const client = new GatewayClient({
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
tlsFingerprint,
|
||||
onConnectError: failure.resolve,
|
||||
});
|
||||
try {
|
||||
client.start();
|
||||
const error = await withTimeout(failure.promise, "Gateway plaintext pin policy");
|
||||
if (!/fingerprint requires wss:\/\//iu.test(error.message)) {
|
||||
throw new Error(`plaintext mismatch failed for an unexpected reason: ${error.message}`);
|
||||
}
|
||||
return error.message;
|
||||
} finally {
|
||||
await client.stopAndWait().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runGatewayTlsPinningProof(): Promise<GatewayTlsPinningProof> {
|
||||
const restoreEnvironment = captureEnvironment();
|
||||
const runtimeRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-tls-pinning-"));
|
||||
const stateDir = path.join(runtimeRoot, "state");
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
const certPath = path.join(runtimeRoot, "tls", "gateway-cert.pem");
|
||||
const keyPath = path.join(runtimeRoot, "tls", "gateway-key.pem");
|
||||
const pluginDir = path.join(runtimeRoot, "discovery-plugin");
|
||||
const advertisementPath = path.join(runtimeRoot, "gateway-discovery-advertisement.json");
|
||||
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
|
||||
|
||||
try {
|
||||
process.env.HOME = runtimeRoot;
|
||||
process.env.OPENCLAW_CONFIG_PATH = configPath;
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
|
||||
delete process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
delete process.env.OPENCLAW_TEST_MINIMAL_GATEWAY;
|
||||
process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1";
|
||||
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
|
||||
process.env.OPENCLAW_SKIP_CHANNELS = "1";
|
||||
process.env.OPENCLAW_SKIP_CRON = "1";
|
||||
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
|
||||
process.env.OPENCLAW_SKIP_PROVIDERS = "1";
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.OPENCLAW_DISABLE_BONJOUR;
|
||||
delete process.env.VITEST;
|
||||
await writeDiscoveryProbePlugin(pluginDir, advertisementPath);
|
||||
|
||||
const preparedTls = await loadGatewayTlsRuntime({
|
||||
enabled: true,
|
||||
autoGenerate: true,
|
||||
certPath,
|
||||
keyPath,
|
||||
});
|
||||
if (!preparedTls.enabled || !preparedTls.fingerprintSha256) {
|
||||
throw new Error(preparedTls.error ?? "Gateway TLS runtime did not expose a fingerprint");
|
||||
}
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
gateway: {
|
||||
auth: { mode: "none" },
|
||||
bind: "loopback",
|
||||
controlUi: { enabled: false },
|
||||
tls: { enabled: true, autoGenerate: false, certPath, keyPath },
|
||||
},
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: [DISCOVERY_PLUGIN_ID],
|
||||
load: { paths: [pluginDir] },
|
||||
entries: {
|
||||
[DISCOVERY_PLUGIN_ID]: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
clearConfigCache();
|
||||
clearRuntimeConfigSnapshot();
|
||||
|
||||
const port = await getFreePort();
|
||||
server = await startGatewayServer(port, {
|
||||
auth: { mode: "none" },
|
||||
bind: "loopback",
|
||||
controlUiEnabled: false,
|
||||
sidecarStartup: "defer",
|
||||
});
|
||||
const url = `wss://127.0.0.1:${port}`;
|
||||
const advertisedFingerprint = await readAdvertisedFingerprint(advertisementPath);
|
||||
const peerFingerprint = await waitForPeerFingerprint(port);
|
||||
if (peerFingerprint !== advertisedFingerprint) {
|
||||
throw new Error("Gateway advertised TLS fingerprint did not match the live peer certificate");
|
||||
}
|
||||
|
||||
const healthResponded = await connectWithExactPin(url, advertisedFingerprint);
|
||||
const wrongPin = `${advertisedFingerprint.slice(0, -1)}${
|
||||
advertisedFingerprint.endsWith("0") ? "1" : "0"
|
||||
}`;
|
||||
const wrongPinResult = await connectWithWrongPin(url, wrongPin);
|
||||
const cleartextMismatch = await proveCleartextMismatch(port, advertisedFingerprint);
|
||||
|
||||
return {
|
||||
advertisedFingerprint,
|
||||
cleartextMismatch,
|
||||
healthResponded,
|
||||
peerFingerprint,
|
||||
wrongPinFailure: wrongPinResult.error,
|
||||
wrongPinHelloObserved: wrongPinResult.helloObserved,
|
||||
};
|
||||
} finally {
|
||||
await server?.close({ reason: "Gateway TLS pinning proof complete" }).catch(() => undefined);
|
||||
clearConfigCache();
|
||||
clearRuntimeConfigSnapshot();
|
||||
restoreEnvironment();
|
||||
await fs.rm(runtimeRoot, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function runGatewayTlsPinningProducer(
|
||||
options: ProducerOptions,
|
||||
): Promise<QaEvidenceSummaryJson> {
|
||||
const writer = createQaScriptEvidenceWriter({
|
||||
artifactBase: options.artifactBase,
|
||||
logFileName: `${SCENARIO_ID}.log`,
|
||||
primaryModel: "mock-openai/gpt-5.6-luna",
|
||||
providerMode: "mock-openai",
|
||||
repoRoot: options.repoRoot,
|
||||
target: {
|
||||
id: SCENARIO_ID,
|
||||
title: "Gateway TLS certificate pinning",
|
||||
sourcePath: SOURCE_PATH,
|
||||
docsRefs: ["docs/gateway/index.md", "docs/gateway/discovery.md", "docs/gateway/protocol.md"],
|
||||
codeRefs: [
|
||||
"src/infra/tls/gateway.ts",
|
||||
"src/gateway/server-runtime-state.ts",
|
||||
"packages/gateway-client/src/client.ts",
|
||||
],
|
||||
},
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const proof = await runGatewayTlsPinningProof();
|
||||
await fs.mkdir(options.artifactBase, { recursive: true });
|
||||
const summaryPath = path.join(options.artifactBase, "gateway-tls-pinning-summary.json");
|
||||
await fs.writeFile(summaryPath, `${JSON.stringify(proof, null, 2)}\n`, "utf8");
|
||||
writer.appendLog("pass: exact pin connected; wrong pin and plaintext mismatch rejected\n");
|
||||
return await writer.write({
|
||||
artifacts: [{ filePath: summaryPath, kind: "summary" }],
|
||||
details: "Real TLS Gateway pinning contract passed.",
|
||||
durationMs: Math.max(1, Date.now() - startedAt),
|
||||
status: "pass",
|
||||
});
|
||||
} catch (error) {
|
||||
const details = formatErrorMessage(error);
|
||||
writer.appendLog(`fail: ${details}\n`);
|
||||
return await writer.write({
|
||||
details,
|
||||
durationMs: Math.max(1, Date.now() - startedAt),
|
||||
status: "fail",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function main(argv: readonly string[]) {
|
||||
const evidence = await runGatewayTlsPinningProducer(parseOptions(argv));
|
||||
const status = evidence.entries[0]?.result.status;
|
||||
console.log(`Gateway TLS pinning evidence: ${QA_EVIDENCE_FILENAME}`);
|
||||
console.log(`Gateway TLS pinning status: ${status}`);
|
||||
return status === "pass" ? 0 : 1;
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
main(process.argv.slice(2))
|
||||
.then((exitCode) => {
|
||||
process.exit(exitCode);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(formatErrorMessage(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user