Redact edge gateway diagnostics
This commit is contained in:
@@ -21,7 +21,7 @@ import {
|
||||
setDepartmentGatewayCutover,
|
||||
updateEdgeGateway,
|
||||
} from "@/services/edgeGateways.js";
|
||||
import { normalizeEdgeGatewayError, normalizeGatewayWebSocketClose } from "@/features/edgeGateways/edgeGatewayErrors.js";
|
||||
import { normalizeEdgeGatewayError, normalizeGatewayWebSocketClose, redactEdgeGatewayDiagnostic } from "@/features/edgeGateways/edgeGatewayErrors.js";
|
||||
import EdgeGatewayOverviewPage from "@/features/edgeGateways/EdgeGatewayOverviewPage.vue";
|
||||
import EdgeGatewayInventoryPage from "@/features/edgeGateways/EdgeGatewayInventoryPage.vue";
|
||||
import EdgeGatewayTasksPage from "@/features/edgeGateways/EdgeGatewayTasksPage.vue";
|
||||
@@ -183,7 +183,7 @@ const terminalGatewayBrokerDiagnostics = (gateway = selectedGatewayView.value) =
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return;
|
||||
}
|
||||
lines.push(`${label}: ${value}`);
|
||||
lines.push(`${label}: ${redactEdgeGatewayDiagnostic(value)}`);
|
||||
};
|
||||
|
||||
push("Gateway ID", gateway?.id);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { redactEdgeGatewayDiagnostic } from "@/features/edgeGateways/edgeGatewayErrors.js";
|
||||
|
||||
const props = defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
@@ -44,7 +46,10 @@ const shortcuts = [
|
||||
{ id: "docker ps", label: "docker ps" },
|
||||
];
|
||||
|
||||
const diagnosticsText = () => (Array.isArray(props.diagnostics) ? props.diagnostics.join("\n") : "");
|
||||
const diagnosticsText = () =>
|
||||
Array.isArray(props.diagnostics)
|
||||
? props.diagnostics.map((entry) => redactEdgeGatewayDiagnostic(entry)).join("\n")
|
||||
: "";
|
||||
|
||||
const copyDiagnostics = async () => {
|
||||
const text = diagnosticsText();
|
||||
|
||||
@@ -55,6 +55,36 @@ const WEBSOCKET_CLOSE_MESSAGES = {
|
||||
socket_error: "Terminal websocket connection failed.",
|
||||
};
|
||||
|
||||
const SENSITIVE_DIAGNOSTIC_KEYS = new Set([
|
||||
"accesstoken",
|
||||
"agenttoken",
|
||||
"apikey",
|
||||
"authorization",
|
||||
"auth",
|
||||
"clientsecret",
|
||||
"jwt",
|
||||
"key",
|
||||
"password",
|
||||
"secret",
|
||||
"signature",
|
||||
"token",
|
||||
]);
|
||||
|
||||
const normalizeDiagnosticKey = (key) =>
|
||||
String(key || "")
|
||||
.replace(/[^a-z0-9]/gi, "")
|
||||
.toLowerCase();
|
||||
|
||||
const isSensitiveDiagnosticKey = (key) => SENSITIVE_DIAGNOSTIC_KEYS.has(normalizeDiagnosticKey(key));
|
||||
|
||||
const redactSensitiveAssignments = (value) =>
|
||||
String(value)
|
||||
.replace(/\b(authorization|auth)\s*[:=]\s*(?:Bearer|Basic)\s+[^\s,;&]+/gi, (_match, key) => `${key}=***`)
|
||||
.replace(
|
||||
/([?&;,\s]?(access[_-]?token|agent[_-]?token|api[_-]?key|authorization|auth|client[_-]?secret|jwt|key|password|secret|signature|token)\s*[:=]\s*)("[^"\s,;&]*"|'[^'\s,;&]*'|[^\s,;&]+)/gi,
|
||||
(_match, prefix) => `${prefix}***`
|
||||
);
|
||||
|
||||
export function redactEdgeGatewayUrl(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -62,17 +92,33 @@ export function redactEdgeGatewayUrl(value) {
|
||||
|
||||
try {
|
||||
const url = new URL(String(value), typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
||||
["token", "agentToken", "agent_token"].forEach((key) => {
|
||||
if (url.searchParams.has(key)) {
|
||||
if (url.username) {
|
||||
url.username = "***";
|
||||
}
|
||||
if (url.password) {
|
||||
url.password = "***";
|
||||
}
|
||||
Array.from(url.searchParams.keys()).forEach((key) => {
|
||||
if (isSensitiveDiagnosticKey(key)) {
|
||||
url.searchParams.set(key, "***");
|
||||
}
|
||||
});
|
||||
return url.toString();
|
||||
} catch (_error) {
|
||||
return String(value).replace(/([?&](?:token|agentToken|agent_token)=)[^&]+/gi, "$1***");
|
||||
return redactSensitiveAssignments(String(value));
|
||||
}
|
||||
}
|
||||
|
||||
export function redactEdgeGatewayDiagnostic(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return redactSensitiveAssignments(
|
||||
String(value).replace(/\b(?:wss?|https?|mqtts?):\/\/[^\s<>"')]+/gi, (match) => redactEdgeGatewayUrl(match))
|
||||
);
|
||||
}
|
||||
|
||||
const formatDiagnosticLines = (diagnostics, prefix = "Diagnostic") => {
|
||||
if (!diagnostics || typeof diagnostics !== "object" || Array.isArray(diagnostics)) {
|
||||
return [];
|
||||
@@ -83,20 +129,23 @@ const formatDiagnosticLines = (diagnostics, prefix = "Diagnostic") => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return;
|
||||
}
|
||||
lines.push(`${label}: ${value}`);
|
||||
lines.push(`${label}: ${redactEdgeGatewayDiagnostic(value)}`);
|
||||
};
|
||||
|
||||
push(`${prefix} code`, diagnostics.reason_code || diagnostics.error_code);
|
||||
push("Broker URL", diagnostics.broker_url);
|
||||
push("WebSocket URL", redactEdgeGatewayUrl(diagnostics.ws_url));
|
||||
push("WebSocket URL", diagnostics.ws_url);
|
||||
push("Derived URL warning", diagnostics.derived_warning);
|
||||
if (diagnostics.broker_presence && typeof diagnostics.broker_presence === "object") {
|
||||
push("Broker connected", diagnostics.broker_presence.connected === true ? "yes" : "no");
|
||||
push("Broker connection", diagnostics.broker_presence.connection_id);
|
||||
push("Broker last seen", diagnostics.broker_presence.last_seen_at);
|
||||
push("Broker age", Number.isFinite(Number(diagnostics.broker_presence.age_seconds))
|
||||
? `${Number(diagnostics.broker_presence.age_seconds)}s`
|
||||
: null);
|
||||
push(
|
||||
"Broker age",
|
||||
Number.isFinite(Number(diagnostics.broker_presence.age_seconds))
|
||||
? `${Number(diagnostics.broker_presence.age_seconds)}s`
|
||||
: null
|
||||
);
|
||||
push("Broker last error", diagnostics.broker_presence.last_error);
|
||||
push("Broker disconnect reason", diagnostics.broker_presence.disconnect_reason);
|
||||
}
|
||||
@@ -180,7 +229,7 @@ export function normalizeGatewayWebSocketClose(event = {}, context = {}) {
|
||||
details.push(`WebSocket code: ${code}`);
|
||||
}
|
||||
if (reason) {
|
||||
details.push(`Close reason: ${reason}`);
|
||||
details.push(`Close reason: ${redactEdgeGatewayDiagnostic(reason)}`);
|
||||
}
|
||||
if (wasClean !== null) {
|
||||
details.push(`Clean close: ${wasClean ? "yes" : "no"}`);
|
||||
|
||||
@@ -2,7 +2,13 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { inferEdgeGatewayErrorCode, normalizeEdgeGatewayError } from "@/features/edgeGateways/edgeGatewayErrors.js";
|
||||
import {
|
||||
inferEdgeGatewayErrorCode,
|
||||
normalizeEdgeGatewayError,
|
||||
normalizeGatewayWebSocketClose,
|
||||
redactEdgeGatewayDiagnostic,
|
||||
redactEdgeGatewayUrl,
|
||||
} from "@/features/edgeGateways/edgeGatewayErrors.js";
|
||||
|
||||
const managerSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayManager.vue"), "utf8");
|
||||
const tasksSource = readFileSync(join(process.cwd(), "src/features/edgeGateways/EdgeGatewayTasksPage.vue"), "utf8");
|
||||
@@ -49,4 +55,47 @@ describe("edge gateway workflow helpers", () => {
|
||||
expect(normalized.title).toMatch(/legitimationsoplysninger/i);
|
||||
expect(normalized.message).toContain("Invalid edge gateway token");
|
||||
});
|
||||
it("redacts broker diagnostic secrets before rendering technical details", () => {
|
||||
expect(
|
||||
redactEdgeGatewayUrl(
|
||||
"wss://brokerUser:brokerPass@broker.internal/socket?access_token=ACCESSSECRET&signature=SIGSECRET&token=BROKER_TOKEN"
|
||||
)
|
||||
).toBe("wss://***:***@broker.internal/socket?access_token=***&signature=***&token=***");
|
||||
|
||||
expect(
|
||||
redactEdgeGatewayDiagnostic(
|
||||
"authorization=Bearer LASTERRSECRET url=wss://u:p@broker.internal/?jwt=ERRJWT signed_url=https://host/path?signature=DISCSIG&key=DISCKEY"
|
||||
)
|
||||
).not.toMatch(/LASTERRSECRET|ERRJWT|DISCSIG|DISCKEY|u:p/);
|
||||
|
||||
const normalized = normalizeEdgeGatewayError({
|
||||
response: {
|
||||
data: {
|
||||
data: {
|
||||
diagnostics: {
|
||||
broker_url:
|
||||
"wss://brokerUser:brokerPass@broker.internal/socket?access_token=ACCESSSECRET&signature=SIGSECRET&token=BROKER_TOKEN",
|
||||
ws_url: "wss://agentUser:agentPass@gw.internal/terminal?token=WS_TOKEN&jwt=JWTSECRET",
|
||||
broker_presence: {
|
||||
last_error: "mqtt failed authorization=Bearer LASTERRSECRET url=wss://u:p@broker.internal/?jwt=ERRJWT",
|
||||
disconnect_reason: "disconnect signed_url=https://host/path?signature=DISCSIG&key=DISCKEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const details = normalized.details.join("\n");
|
||||
expect(details).toContain("Broker URL: wss://***:***@broker.internal/socket?access_token=***");
|
||||
expect(details).toContain("WebSocket URL: wss://***:***@gw.internal/terminal?token=***&jwt=***");
|
||||
expect(details).not.toMatch(
|
||||
/brokerUser|brokerPass|ACCESSSECRET|SIGSECRET|BROKER_TOKEN|agentUser|agentPass|WS_TOKEN|JWTSECRET|LASTERRSECRET|ERRJWT|DISCSIG|DISCKEY|u:p/
|
||||
);
|
||||
|
||||
const close = normalizeGatewayWebSocketClose({
|
||||
reason: "disconnect signed_url=https://host/path?signature=DISCSIG&key=DISCKEY",
|
||||
});
|
||||
expect(close.details.join("\n")).not.toMatch(/DISCSIG|DISCKEY/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user