mcp-server-kubernetes 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/server-config.d.ts +1 -1
- package/dist/config/server-config.js +1 -1
- package/dist/index.d.ts +21 -2
- package/dist/security/transport.d.ts +1 -0
- package/dist/security/transport.js +17 -0
- package/dist/tools/kubectl-create.d.ts +25 -2
- package/dist/tools/kubectl-create.js +87 -27
- package/dist/tools/kubectl-get.d.ts +23 -0
- package/dist/tools/kubectl-get.js +69 -12
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -616,7 +616,7 @@ declare const allTools: ({
|
|
|
616
616
|
};
|
|
617
617
|
readonly filename: {
|
|
618
618
|
readonly type: "string";
|
|
619
|
-
readonly description: "Path to a YAML file to create resources from";
|
|
619
|
+
readonly description: "Path to a YAML file to create resources from. The path is read on the machine running the MCP server, so it is rejected when the server runs over a remote (SSE/Streamable HTTP) transport; use 'manifest' to pass the file's contents instead.";
|
|
620
620
|
};
|
|
621
621
|
readonly resourceType: {
|
|
622
622
|
readonly type: "string";
|
|
@@ -643,7 +643,26 @@ declare const allTools: ({
|
|
|
643
643
|
readonly items: {
|
|
644
644
|
readonly type: "string";
|
|
645
645
|
};
|
|
646
|
-
readonly description: "Path to file for creating configmap (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"])";
|
|
646
|
+
readonly description: "Path to file for creating configmap/secret (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"]). The path is read on the machine running the MCP server, so it is rejected when the server runs over a remote (SSE/Streamable HTTP) transport; use \"fromFileContent\" to pass file contents directly instead.";
|
|
647
|
+
};
|
|
648
|
+
readonly fromFileContent: {
|
|
649
|
+
readonly type: "array";
|
|
650
|
+
readonly items: {
|
|
651
|
+
readonly type: "object";
|
|
652
|
+
readonly properties: {
|
|
653
|
+
readonly key: {
|
|
654
|
+
readonly type: "string";
|
|
655
|
+
readonly description: "Key to store the content under in the configmap/secret";
|
|
656
|
+
};
|
|
657
|
+
readonly content: {
|
|
658
|
+
readonly type: "string";
|
|
659
|
+
readonly description: "The file content to store";
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
readonly required: readonly ["key", "content"];
|
|
663
|
+
readonly additionalProperties: false;
|
|
664
|
+
};
|
|
665
|
+
readonly description: "Inline file contents for creating a configmap/secret, provided by the client instead of a server-side path (e.g. [{\"key\": \"app.conf\", \"content\": \"...\"}]). Safe on all transports; use this instead of \"fromFile\" on remote (SSE/Streamable HTTP) servers.";
|
|
647
666
|
};
|
|
648
667
|
readonly secretType: {
|
|
649
668
|
readonly type: "string";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isRemoteTransport(): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Transport-mode detection for security decisions.
|
|
2
|
+
//
|
|
3
|
+
// The server selects its transport at startup based on these env vars (see
|
|
4
|
+
// src/index.ts): if either is set the server listens over HTTP (SSE or
|
|
5
|
+
// Streamable HTTP) and serves remote clients; otherwise it uses stdio and is
|
|
6
|
+
// driven by a local process that already runs as the operator.
|
|
7
|
+
//
|
|
8
|
+
// Some tool inputs are only safe under the stdio trust model. A server-side
|
|
9
|
+
// filesystem path (e.g. `--from-file` / `-f`) is read on the machine running
|
|
10
|
+
// the server: over stdio that machine is the operator's own, but over HTTP it
|
|
11
|
+
// is the server host, so any client that can reach the endpoint could read
|
|
12
|
+
// arbitrary files (kubeconfig, service-account token, /proc/self/environ).
|
|
13
|
+
// Guards use this helper to reject path-based reads on remote transports.
|
|
14
|
+
export function isRemoteTransport() {
|
|
15
|
+
return Boolean(process.env.ENABLE_UNSAFE_SSE_TRANSPORT ||
|
|
16
|
+
process.env.ENABLE_UNSAFE_STREAMABLE_HTTP_TRANSPORT);
|
|
17
|
+
}
|
|
@@ -27,7 +27,7 @@ export declare const kubectlCreateSchema: {
|
|
|
27
27
|
};
|
|
28
28
|
readonly filename: {
|
|
29
29
|
readonly type: "string";
|
|
30
|
-
readonly description: "Path to a YAML file to create resources from";
|
|
30
|
+
readonly description: "Path to a YAML file to create resources from. The path is read on the machine running the MCP server, so it is rejected when the server runs over a remote (SSE/Streamable HTTP) transport; use 'manifest' to pass the file's contents instead.";
|
|
31
31
|
};
|
|
32
32
|
readonly resourceType: {
|
|
33
33
|
readonly type: "string";
|
|
@@ -54,7 +54,26 @@ export declare const kubectlCreateSchema: {
|
|
|
54
54
|
readonly items: {
|
|
55
55
|
readonly type: "string";
|
|
56
56
|
};
|
|
57
|
-
readonly description: "Path to file for creating configmap (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"])";
|
|
57
|
+
readonly description: "Path to file for creating configmap/secret (e.g. [\"key1=/path/to/file1\", \"key2=/path/to/file2\"]). The path is read on the machine running the MCP server, so it is rejected when the server runs over a remote (SSE/Streamable HTTP) transport; use \"fromFileContent\" to pass file contents directly instead.";
|
|
58
|
+
};
|
|
59
|
+
readonly fromFileContent: {
|
|
60
|
+
readonly type: "array";
|
|
61
|
+
readonly items: {
|
|
62
|
+
readonly type: "object";
|
|
63
|
+
readonly properties: {
|
|
64
|
+
readonly key: {
|
|
65
|
+
readonly type: "string";
|
|
66
|
+
readonly description: "Key to store the content under in the configmap/secret";
|
|
67
|
+
};
|
|
68
|
+
readonly content: {
|
|
69
|
+
readonly type: "string";
|
|
70
|
+
readonly description: "The file content to store";
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
readonly required: readonly ["key", "content"];
|
|
74
|
+
readonly additionalProperties: false;
|
|
75
|
+
};
|
|
76
|
+
readonly description: "Inline file contents for creating a configmap/secret, provided by the client instead of a server-side path (e.g. [{\"key\": \"app.conf\", \"content\": \"...\"}]). Safe on all transports; use this instead of \"fromFile\" on remote (SSE/Streamable HTTP) servers.";
|
|
58
77
|
};
|
|
59
78
|
readonly secretType: {
|
|
60
79
|
readonly type: "string";
|
|
@@ -136,6 +155,10 @@ export declare function kubectlCreate(k8sManager: KubernetesManager, input: {
|
|
|
136
155
|
namespace?: string;
|
|
137
156
|
fromLiteral?: string[];
|
|
138
157
|
fromFile?: string[];
|
|
158
|
+
fromFileContent?: {
|
|
159
|
+
key: string;
|
|
160
|
+
content: string;
|
|
161
|
+
}[];
|
|
139
162
|
secretType?: "generic" | "docker-registry" | "tls";
|
|
140
163
|
serviceType?: "clusterip" | "nodeport" | "loadbalancer" | "externalname";
|
|
141
164
|
tcpPort?: string[];
|
|
@@ -5,6 +5,7 @@ import * as path from "path";
|
|
|
5
5
|
import * as os from "os";
|
|
6
6
|
import { getSpawnMaxBuffer } from "../config/max-buffer.js";
|
|
7
7
|
import { contextParameter, namespaceParameter, dryRunParameter } from "../models/common-parameters.js";
|
|
8
|
+
import { isRemoteTransport } from "../security/transport.js";
|
|
8
9
|
export const kubectlCreateSchema = {
|
|
9
10
|
name: "kubectl_create",
|
|
10
11
|
description: "Create Kubernetes resources using various methods (from file or using subcommands)",
|
|
@@ -42,7 +43,7 @@ export const kubectlCreateSchema = {
|
|
|
42
43
|
},
|
|
43
44
|
filename: {
|
|
44
45
|
type: "string",
|
|
45
|
-
description: "Path to a YAML file to create resources from",
|
|
46
|
+
description: "Path to a YAML file to create resources from. The path is read on the machine running the MCP server, so it is rejected when the server runs over a remote (SSE/Streamable HTTP) transport; use 'manifest' to pass the file's contents instead.",
|
|
46
47
|
},
|
|
47
48
|
// Resource type to create (determines which subcommand to use)
|
|
48
49
|
resourceType: {
|
|
@@ -64,7 +65,26 @@ export const kubectlCreateSchema = {
|
|
|
64
65
|
fromFile: {
|
|
65
66
|
type: "array",
|
|
66
67
|
items: { type: "string" },
|
|
67
|
-
description: 'Path to file for creating configmap (e.g. ["key1=/path/to/file1", "key2=/path/to/file2"])',
|
|
68
|
+
description: 'Path to file for creating configmap/secret (e.g. ["key1=/path/to/file1", "key2=/path/to/file2"]). The path is read on the machine running the MCP server, so it is rejected when the server runs over a remote (SSE/Streamable HTTP) transport; use "fromFileContent" to pass file contents directly instead.',
|
|
69
|
+
},
|
|
70
|
+
fromFileContent: {
|
|
71
|
+
type: "array",
|
|
72
|
+
items: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
key: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Key to store the content under in the configmap/secret",
|
|
78
|
+
},
|
|
79
|
+
content: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "The file content to store",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ["key", "content"],
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
},
|
|
87
|
+
description: 'Inline file contents for creating a configmap/secret, provided by the client instead of a server-side path (e.g. [{"key": "app.conf", "content": "..."}]). Safe on all transports; use this instead of "fromFile" on remote (SSE/Streamable HTTP) servers.',
|
|
68
88
|
},
|
|
69
89
|
// Namespace specific parameters
|
|
70
90
|
// No special parameters for namespace, just name is needed
|
|
@@ -143,6 +163,21 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
143
163
|
input.resourceType !== "namespace") {
|
|
144
164
|
throw new McpError(ErrorCode.InvalidRequest, `Name is required when creating a ${input.resourceType}`);
|
|
145
165
|
}
|
|
166
|
+
// Reject server-side filesystem reads on remote transports. Over SSE /
|
|
167
|
+
// Streamable HTTP the path resolves on the MCP server host, not the
|
|
168
|
+
// client, so `filename` (-f) and `fromFile` (--from-file) would let any
|
|
169
|
+
// client that can reach the endpoint read arbitrary server files
|
|
170
|
+
// (kubeconfig, service-account token, /proc/self/environ, etc.). See
|
|
171
|
+
// GHSA-m67f-jxm9-cvx8. Clients on these transports must pass content
|
|
172
|
+
// inline via `manifest` or `fromFileContent` instead.
|
|
173
|
+
if (isRemoteTransport()) {
|
|
174
|
+
if (input.filename) {
|
|
175
|
+
throw new McpError(ErrorCode.InvalidRequest, "The 'filename' parameter reads a file from the MCP server's filesystem and is disabled on remote (SSE/Streamable HTTP) transports. Pass the file contents via 'manifest' instead.");
|
|
176
|
+
}
|
|
177
|
+
if (input.fromFile && input.fromFile.length > 0) {
|
|
178
|
+
throw new McpError(ErrorCode.InvalidRequest, "The 'fromFile' parameter reads files from the MCP server's filesystem and is disabled on remote (SSE/Streamable HTTP) transports. Pass the file contents via 'fromFileContent' instead.");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
146
181
|
// Set up common parameters
|
|
147
182
|
const namespace = input.namespace || "default";
|
|
148
183
|
const dryRun = input.dryRun || false;
|
|
@@ -151,15 +186,33 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
151
186
|
const context = input.context || "";
|
|
152
187
|
const command = "kubectl";
|
|
153
188
|
const args = ["create"];
|
|
154
|
-
|
|
189
|
+
const tempFiles = [];
|
|
190
|
+
// Write client-provided content to a private temp file and return its
|
|
191
|
+
// path. The file is tracked for cleanup after kubectl runs.
|
|
192
|
+
const writeTempFile = (contents, label, ext = "") => {
|
|
193
|
+
const tmpDir = os.tmpdir();
|
|
194
|
+
const tempFile = path.join(tmpDir, `create-${label}-${Date.now()}-${tempFiles.length}${ext}`);
|
|
195
|
+
fs.writeFileSync(tempFile, contents, { mode: 0o600 });
|
|
196
|
+
tempFiles.push(tempFile);
|
|
197
|
+
return tempFile;
|
|
198
|
+
};
|
|
199
|
+
// Emit `--from-file=<key>=<tempfile>` args for inline content supplied by
|
|
200
|
+
// the client. Each entry's content is written to a server-side temp file
|
|
201
|
+
// (never a client-controlled path), so this is safe on all transports.
|
|
202
|
+
const pushFromFileContent = (entries) => {
|
|
203
|
+
entries.forEach((entry) => {
|
|
204
|
+
if (!entry.key) {
|
|
205
|
+
throw new McpError(ErrorCode.InvalidRequest, "Each fromFileContent entry requires a non-empty 'key'");
|
|
206
|
+
}
|
|
207
|
+
const tempFile = writeTempFile(entry.content ?? "", "fromfile");
|
|
208
|
+
args.push(`--from-file=${entry.key}=${tempFile}`);
|
|
209
|
+
});
|
|
210
|
+
};
|
|
155
211
|
// Process manifest content if provided (file-based creation)
|
|
156
212
|
if (input.manifest || input.filename) {
|
|
157
213
|
if (input.manifest) {
|
|
158
214
|
// Create temporary file for the manifest
|
|
159
|
-
|
|
160
|
-
tempFile = path.join(tmpDir, `create-manifest-${Date.now()}.yaml`);
|
|
161
|
-
fs.writeFileSync(tempFile, input.manifest);
|
|
162
|
-
args.push("-f", tempFile);
|
|
215
|
+
args.push("-f", writeTempFile(input.manifest, "manifest", ".yaml"));
|
|
163
216
|
}
|
|
164
217
|
else if (input.filename) {
|
|
165
218
|
args.push("-f", input.filename);
|
|
@@ -179,12 +232,17 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
179
232
|
args.push(`--from-literal=${literal}`);
|
|
180
233
|
});
|
|
181
234
|
}
|
|
182
|
-
// Add --from-file arguments
|
|
235
|
+
// Add --from-file arguments (server-side paths; blocked on remote
|
|
236
|
+
// transports by the guard above)
|
|
183
237
|
if (input.fromFile && input.fromFile.length > 0) {
|
|
184
238
|
input.fromFile.forEach((file) => {
|
|
185
239
|
args.push(`--from-file=${file}`);
|
|
186
240
|
});
|
|
187
241
|
}
|
|
242
|
+
// Add inline file contents (safe on all transports)
|
|
243
|
+
if (input.fromFileContent && input.fromFileContent.length > 0) {
|
|
244
|
+
pushFromFileContent(input.fromFileContent);
|
|
245
|
+
}
|
|
188
246
|
break;
|
|
189
247
|
case "secret":
|
|
190
248
|
if (!input.secretType) {
|
|
@@ -197,12 +255,17 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
197
255
|
args.push(`--from-literal=${literal}`);
|
|
198
256
|
});
|
|
199
257
|
}
|
|
200
|
-
// Add --from-file arguments
|
|
258
|
+
// Add --from-file arguments (server-side paths; blocked on remote
|
|
259
|
+
// transports by the guard above)
|
|
201
260
|
if (input.fromFile && input.fromFile.length > 0) {
|
|
202
261
|
input.fromFile.forEach((file) => {
|
|
203
262
|
args.push(`--from-file=${file}`);
|
|
204
263
|
});
|
|
205
264
|
}
|
|
265
|
+
// Add inline file contents (safe on all transports)
|
|
266
|
+
if (input.fromFileContent && input.fromFileContent.length > 0) {
|
|
267
|
+
pushFromFileContent(input.fromFileContent);
|
|
268
|
+
}
|
|
206
269
|
break;
|
|
207
270
|
case "service":
|
|
208
271
|
if (!input.serviceType) {
|
|
@@ -292,6 +355,17 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
292
355
|
if (context) {
|
|
293
356
|
args.push("--context", context);
|
|
294
357
|
}
|
|
358
|
+
// Remove any temp files created for inline content.
|
|
359
|
+
const cleanupTempFiles = () => {
|
|
360
|
+
tempFiles.forEach((tempFile) => {
|
|
361
|
+
try {
|
|
362
|
+
fs.unlinkSync(tempFile);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.warn(`Failed to delete temporary file ${tempFile}: ${err}`);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
};
|
|
295
369
|
// Execute the command
|
|
296
370
|
try {
|
|
297
371
|
const result = execFileSyncSafe(command, args, {
|
|
@@ -299,15 +373,8 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
299
373
|
maxBuffer: getSpawnMaxBuffer(),
|
|
300
374
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
301
375
|
});
|
|
302
|
-
// Clean up temp
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
fs.unlinkSync(tempFile);
|
|
306
|
-
}
|
|
307
|
-
catch (err) {
|
|
308
|
-
console.warn(`Failed to delete temporary file ${tempFile}: ${err}`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
376
|
+
// Clean up temp files if created
|
|
377
|
+
cleanupTempFiles();
|
|
311
378
|
return {
|
|
312
379
|
content: [
|
|
313
380
|
{
|
|
@@ -318,15 +385,8 @@ export async function kubectlCreate(k8sManager, input) {
|
|
|
318
385
|
};
|
|
319
386
|
}
|
|
320
387
|
catch (error) {
|
|
321
|
-
// Clean up temp
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
fs.unlinkSync(tempFile);
|
|
325
|
-
}
|
|
326
|
-
catch (err) {
|
|
327
|
-
console.warn(`Failed to delete temporary file ${tempFile}: ${err}`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
388
|
+
// Clean up temp files if created, even if command failed
|
|
389
|
+
cleanupTempFiles();
|
|
330
390
|
throw new McpError(ErrorCode.InternalError, `Failed to create resource: ${error.message}`);
|
|
331
391
|
}
|
|
332
392
|
}
|
|
@@ -76,3 +76,26 @@ export declare function kubectlGet(k8sManager: KubernetesManager, input: {
|
|
|
76
76
|
}[];
|
|
77
77
|
isError: boolean;
|
|
78
78
|
}>;
|
|
79
|
+
/**
|
|
80
|
+
* Determine whether a (already lower-cased) resourceType string references
|
|
81
|
+
* Kubernetes Secrets in any of the syntaxes kubectl accepts. This backs the
|
|
82
|
+
* masking decision, so it must recognize every way a caller could name a
|
|
83
|
+
* Secret without tripping the naive "=== 'secret'" check:
|
|
84
|
+
* - plain: "secret", "secrets"
|
|
85
|
+
* - resource/name: "secret/my-secret"
|
|
86
|
+
* - group-qualified: "secrets.v1.", "secret.example.com/my-secret"
|
|
87
|
+
* - comma-separated: "secret,configmap"
|
|
88
|
+
*
|
|
89
|
+
* @param {string} resourceType - The lower-cased resourceType argument.
|
|
90
|
+
* @returns {boolean} True if any referenced resource is a Secret.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resourceReferencesSecret(resourceType: string): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Masks sensitive data in Kubernetes secrets by parsing the raw output and replacing
|
|
95
|
+
* all leaf values in the "data" section with a placeholder value ("***").
|
|
96
|
+
*
|
|
97
|
+
* @param {string} output - The raw output from a `kubectl` command, containing secrets data.
|
|
98
|
+
* @param {string} format - The format of the output, either "json" or "yaml".
|
|
99
|
+
* @returns {string} - The masked output in the same format as the input.
|
|
100
|
+
*/
|
|
101
|
+
export declare function maskSecretsData(output: string, format: string): string;
|
|
@@ -126,9 +126,13 @@ export async function kubectlGet(k8sManager, input) {
|
|
|
126
126
|
maxBuffer: getSpawnMaxBuffer(),
|
|
127
127
|
env: { ...process.env, KUBECONFIG: process.env.KUBECONFIG },
|
|
128
128
|
});
|
|
129
|
-
// Apply secrets masking if enabled and dealing with secrets
|
|
129
|
+
// Apply secrets masking if enabled and dealing with secrets.
|
|
130
|
+
// resourceReferencesSecret normalizes combined forms like
|
|
131
|
+
// "secret/my-secret", group-qualified "secrets.v1./my-secret", and
|
|
132
|
+
// comma-separated lists ("secret,configmap") so the masking decision
|
|
133
|
+
// cannot be bypassed by addressing a Secret through an alternate syntax.
|
|
130
134
|
const shouldMaskSecrets = process.env.MASK_SECRETS !== "false" &&
|
|
131
|
-
(resourceType
|
|
135
|
+
resourceReferencesSecret(resourceType);
|
|
132
136
|
let processedResult = result;
|
|
133
137
|
if (shouldMaskSecrets) {
|
|
134
138
|
processedResult = maskSecretsData(result, output);
|
|
@@ -356,10 +360,54 @@ function isNonNamespacedResource(resourceType) {
|
|
|
356
360
|
return nonNamespacedResources.includes(resourceType.toLowerCase());
|
|
357
361
|
}
|
|
358
362
|
/**
|
|
359
|
-
*
|
|
363
|
+
* Determine whether a (already lower-cased) resourceType string references
|
|
364
|
+
* Kubernetes Secrets in any of the syntaxes kubectl accepts. This backs the
|
|
365
|
+
* masking decision, so it must recognize every way a caller could name a
|
|
366
|
+
* Secret without tripping the naive "=== 'secret'" check:
|
|
367
|
+
* - plain: "secret", "secrets"
|
|
368
|
+
* - resource/name: "secret/my-secret"
|
|
369
|
+
* - group-qualified: "secrets.v1.", "secret.example.com/my-secret"
|
|
370
|
+
* - comma-separated: "secret,configmap"
|
|
371
|
+
*
|
|
372
|
+
* @param {string} resourceType - The lower-cased resourceType argument.
|
|
373
|
+
* @returns {boolean} True if any referenced resource is a Secret.
|
|
374
|
+
*/
|
|
375
|
+
export function resourceReferencesSecret(resourceType) {
|
|
376
|
+
return resourceType
|
|
377
|
+
.split(",")
|
|
378
|
+
.map((part) => {
|
|
379
|
+
// Drop the "/name" portion of a resource/name reference, then the
|
|
380
|
+
// ".group"/".version" suffix of a group-qualified reference.
|
|
381
|
+
const resource = part.trim().split("/")[0].split(".")[0];
|
|
382
|
+
return resource.toLowerCase();
|
|
383
|
+
})
|
|
384
|
+
.some((resource) => resource === "secret" || resource === "secrets");
|
|
385
|
+
}
|
|
386
|
+
// Mask the leaf values of a single Secret object's `data` (and write-only
|
|
387
|
+
// `stringData`) fields, leaving metadata and every other field intact.
|
|
388
|
+
function maskSecretObject(secret) {
|
|
389
|
+
const result = {};
|
|
390
|
+
for (const key in secret) {
|
|
391
|
+
if ((key === "data" || key === "stringData") &&
|
|
392
|
+
typeof secret[key] === "object" &&
|
|
393
|
+
secret[key] !== null) {
|
|
394
|
+
result[key] = maskAllLeafValues(secret[key]);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
result[key] = maskDataValues(secret[key]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Recursively traverses a parsed kubectl response and masks the `data` values
|
|
404
|
+
* of Kubernetes Secrets only. Masking is scoped by object kind rather than by
|
|
405
|
+
* the presence of a "data" key, so that Secret values are always masked (even
|
|
406
|
+
* inside a mixed `List` returned by e.g. `kubectl get secret,configmap`) while
|
|
407
|
+
* non-Secret resources such as ConfigMaps keep their data.
|
|
360
408
|
*
|
|
361
409
|
* @param {any} obj - The object to traverse. Can be an array, object, or primitive value.
|
|
362
|
-
* @returns {any} A new object with masked values in 'data' sections.
|
|
410
|
+
* @returns {any} A new object with masked values in Secret 'data' sections.
|
|
363
411
|
*/
|
|
364
412
|
function maskDataValues(obj) {
|
|
365
413
|
if (obj == null) {
|
|
@@ -369,15 +417,24 @@ function maskDataValues(obj) {
|
|
|
369
417
|
return obj.map((item) => maskDataValues(item));
|
|
370
418
|
}
|
|
371
419
|
if (typeof obj === "object") {
|
|
420
|
+
const kind = typeof obj.kind === "string" ? obj.kind.toLowerCase() : undefined;
|
|
421
|
+
// A single Secret object.
|
|
422
|
+
if (kind === "secret") {
|
|
423
|
+
return maskSecretObject(obj);
|
|
424
|
+
}
|
|
425
|
+
// A SecretList: every item is a Secret, even when kubectl omits the
|
|
426
|
+
// per-item `kind` field in list output, so mask them unconditionally.
|
|
427
|
+
if (kind === "secretlist" && Array.isArray(obj.items)) {
|
|
428
|
+
return {
|
|
429
|
+
...obj,
|
|
430
|
+
items: obj.items.map((item) => maskSecretObject(item)),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// Any other object (including a heterogeneous "List"): recurse so nested
|
|
434
|
+
// Secret objects are still masked without touching non-Secret data.
|
|
372
435
|
const result = {};
|
|
373
436
|
for (const key in obj) {
|
|
374
|
-
|
|
375
|
-
// This is a data section - mask all leaf values within it
|
|
376
|
-
result[key] = maskAllLeafValues(obj[key]);
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
result[key] = maskDataValues(obj[key]);
|
|
380
|
-
}
|
|
437
|
+
result[key] = maskDataValues(obj[key]);
|
|
381
438
|
}
|
|
382
439
|
return result;
|
|
383
440
|
}
|
|
@@ -415,7 +472,7 @@ function maskAllLeafValues(obj) {
|
|
|
415
472
|
* @param {string} format - The format of the output, either "json" or "yaml".
|
|
416
473
|
* @returns {string} - The masked output in the same format as the input.
|
|
417
474
|
*/
|
|
418
|
-
function maskSecretsData(output, format) {
|
|
475
|
+
export function maskSecretsData(output, format) {
|
|
419
476
|
try {
|
|
420
477
|
if (format === "json") {
|
|
421
478
|
const parsed = JSON.parse(output);
|