134 lines
6.9 KiB
JavaScript
134 lines
6.9 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const root = path.join(__dirname, "..");
|
|
require("../src/services/db").migrate();
|
|
const diagnostics = require("../src/services/production-diagnostics");
|
|
const proxySecurity = require("../src/services/proxy-security");
|
|
|
|
assert.deepEqual(Object.keys(diagnostics.CHECKS), [
|
|
"system_health",
|
|
"update_state",
|
|
"plugins",
|
|
"recent_errors",
|
|
"benchmark"
|
|
]);
|
|
|
|
const key = "lumi_diag_verification-key_123";
|
|
const hash = diagnostics.hashAccessKey(key);
|
|
assert.match(hash, /^[a-f0-9]{64}$/);
|
|
assert.equal(diagnostics.verifyAccessKey(key, hash), true);
|
|
assert.equal(diagnostics.verifyAccessKey(`${key}x`, hash), false);
|
|
assert.equal(diagnostics.verifyAccessKey("wrong-prefix", hash), false);
|
|
assert.equal(diagnostics.isSecureDiagnosticRequest({ secure: true, ip: "203.0.113.10" }), true);
|
|
assert.equal(diagnostics.isSecureDiagnosticRequest({ secure: false, ip: "127.0.0.1" }), true);
|
|
assert.equal(diagnostics.isSecureDiagnosticRequest({ secure: false, ip: "203.0.113.10" }), false);
|
|
const proxiedRequest = {
|
|
secure: false,
|
|
protocol: "http",
|
|
socket: { remoteAddress: "::ffff:172.19.0.4" },
|
|
get(name) { return name === "x-forwarded-proto" ? "https" : ""; }
|
|
};
|
|
assert.equal(diagnostics.isTrustedPrivateProxyAddress("172.19.0.4"), true);
|
|
assert.equal(diagnostics.isTrustedPrivateProxyAddress("192.168.1.10"), true);
|
|
assert.equal(diagnostics.isTrustedPrivateProxyAddress("203.0.113.10"), false);
|
|
assert.equal(diagnostics.diagnosticsRequestProtocol(proxiedRequest), "https");
|
|
assert.equal(diagnostics.isSecureDiagnosticRequest(proxiedRequest), true);
|
|
assert.equal(diagnostics.diagnosticsRequestProtocol({ ...proxiedRequest, socket: { remoteAddress: "203.0.113.10" } }), "http");
|
|
const companionDownloadRequest = {
|
|
protocol: "http",
|
|
secure: false,
|
|
socket: { remoteAddress: "::ffff:172.19.0.4" },
|
|
get(name) {
|
|
if (name === "x-forwarded-proto") return "https";
|
|
if (name === "host") return "lumi.example.test";
|
|
return "";
|
|
}
|
|
};
|
|
assert.equal(proxySecurity.requestOrigin(companionDownloadRequest), "https://lumi.example.test");
|
|
assert.equal(proxySecurity.isSecureRequest(companionDownloadRequest), true);
|
|
assert.equal(proxySecurity.isTrustedProxyAddress("172.19.0.4"), true);
|
|
assert.equal(proxySecurity.isTrustedProxyAddress("203.0.113.10"), false);
|
|
assert.equal(proxySecurity.requestOrigin({
|
|
...companionDownloadRequest,
|
|
socket: { remoteAddress: "203.0.113.10" }
|
|
}), "http://lumi.example.test", "a public client must not be able to forge HTTPS");
|
|
assert.equal(proxySecurity.requestProtocol({
|
|
...companionDownloadRequest,
|
|
get(name) { return name === "x-forwarded-proto" ? "javascript" : "lumi.example.test"; }
|
|
}), "http", "unsupported forwarding schemes must be rejected");
|
|
|
|
const redacted = diagnostics.redactDiagnosticValue({
|
|
token: "top-secret",
|
|
message: `authorization=Bearer-value ${key} /mnt/c/private/lumi/file.js user@example.com ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefgh`,
|
|
nested: { password: "also-secret", url: "https://example.test/?api_key=secret-value&ok=1" }
|
|
});
|
|
const redactedJson = JSON.stringify(redacted);
|
|
for (const forbidden of ["top-secret", "also-secret", key, "/mnt/c/private", "user@example.com", "secret-value", "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefgh"]) {
|
|
assert.equal(redactedJson.includes(forbidden), false, `diagnostic output leaked ${forbidden}`);
|
|
}
|
|
assert.match(redactedJson, /redacted|diagnostics key|local path|email/i);
|
|
|
|
const fingerprint = `verify-${Date.now()}`;
|
|
for (let index = 0; index < diagnostics.MAX_REQUESTS_PER_MINUTE; index += 1) {
|
|
const rate = diagnostics.consumeRateLimit(fingerprint, 120000);
|
|
assert.equal(rate.allowed, true);
|
|
}
|
|
assert.equal(diagnostics.consumeRateLimit(fingerprint, 120000).allowed, false);
|
|
assert.equal(diagnostics.consumeRateLimit(fingerprint, 180000).allowed, true);
|
|
|
|
for (const check of Object.keys(diagnostics.CHECKS)) {
|
|
const result = diagnostics.runDiagnosticCheck(check);
|
|
assert.equal(result.schema_version, 1);
|
|
assert.equal(result.check, check);
|
|
assert.equal(typeof result.duration_ms, "number");
|
|
assert.doesNotMatch(JSON.stringify(result), /\blumi_diag_[A-Za-z0-9_-]+\b/);
|
|
}
|
|
assert.throws(() => diagnostics.runDiagnosticCheck("shell"), /Unknown diagnostic check/);
|
|
|
|
const serviceSource = fs.readFileSync(path.join(root, "src", "services", "production-diagnostics.js"), "utf8");
|
|
for (const forbidden of [
|
|
/child_process/,
|
|
/\bexec(?:Sync)?\s*\(/,
|
|
/\bspawn(?:Sync)?\s*\(/,
|
|
/\beval\s*\(/,
|
|
/new\s+Function\s*\(/,
|
|
/\bfetch\s*\(/,
|
|
/require\(["'](?:https?|net|tls|dgram)["']\)/
|
|
]) {
|
|
assert.doesNotMatch(serviceSource, forbidden, "diagnostics must not expose execution or network primitives");
|
|
}
|
|
|
|
const serverSource = fs.readFileSync(path.join(root, "src", "web", "server.js"), "utf8");
|
|
const endpointIndex = serverSource.indexOf('app.post("/api/diagnostics/v1/run"');
|
|
const configuredIndex = serverSource.indexOf("app.use(requireConfigured)");
|
|
assert(endpointIndex > 0 && endpointIndex < configuredIndex, "diagnostics endpoint must remain available for production recovery");
|
|
assert.match(serverSource, /authenticateDiagnosticsRequest\(req\)/);
|
|
assert.match(serverSource, /app\.set\("trust proxy", isTrustedProxyAddress\)/);
|
|
assert.doesNotMatch(serverSource, /LUMI_LOCALHOST/, "proxy trust must not depend on an unrelated undocumented environment flag");
|
|
assert.match(serverSource, /app\.get\("\/admin\/diagnostics", requireRole\("admin"\)/);
|
|
assert.match(serverSource, /app\.post\("\/admin\/diagnostics\/access\/renew", requireRole\("admin"\)/);
|
|
assert.match(serverSource, /app\.post\("\/admin\/diagnostics\/access\/revoke", requireRole\("admin"\)/);
|
|
|
|
const viewSource = fs.readFileSync(path.join(root, "src", "web", "views", "admin-diagnostics.ejs"), "utf8");
|
|
assert.match(viewSource, /no remote control/i);
|
|
assert.match(viewSource, /data-confirm-mode="modal"/);
|
|
assert.match(viewSource, /\.secrets/);
|
|
assert.match(viewSource, /diagnosticsEndpointUrl/);
|
|
assert.match(viewSource, /diagnosticsBaseUrl/);
|
|
assert.doesNotMatch(viewSource, /https:\/\/your-lumi-host/);
|
|
assert.match(serverSource, /const diagnosticsBaseUrl = `\$\{diagnosticsRequestProtocol\(req\)\}:\/\/\$\{req\.get\("host"\)\}`/);
|
|
|
|
const clientSource = fs.readFileSync(path.join(root, "scripts", "production-diagnostics-client.js"), "utf8");
|
|
assert.match(clientSource, /path\.join\(root, "\.secrets"\)/);
|
|
assert.match(clientSource, /path\.join\(defaultSecretPath, "production-diagnostics\.json"\)/);
|
|
assert.match(clientSource, /method: "POST"/);
|
|
assert.match(clientSource, /new URL\("\/api\/diagnostics\/v1\/run"/);
|
|
assert.match(clientSource, /LUMI_PROD_URL/);
|
|
assert.match(clientSource, /LUMI_DIAG_KEY/);
|
|
assert.match(clientSource, /function parseConfig\(raw\)/);
|
|
assert.doesNotMatch(clientSource, /console\.(?:log|error)\(\s*key\s*\)/, "client must not print its access key");
|
|
|
|
console.log("Production diagnostics verification passed: fixed read-only checks, redaction, key validation, rate limiting, HTTPS proxy trust, and admin-only controls.");
|