import * as k8s from '@kubernetes/client-node'; import { BaseResourceOperations, ResourceOperationOptions, WatchCallback, WatchEventType, } from '../BaseResourceOperations.js'; import { KubernetesClient } from '../KubernetesClient.js'; import { isSensitiveMaskEnabled } from '../../utils/SensitiveData.js'; /** * ConfigMap operations implementation - Read-only operations */ export class ConfigMapOperations extends BaseResourceOperations { /** * Regular expressions for detecting sensitive data patterns in keys */ private readonly sensitiveKeyPatterns: RegExp[] = [ // Generic credentials /(?:password|passwd|pwd)/gi, /(?:secret|token)/gi, /(?:api[_-]?(?:key|token|secret|password))/gi, /(?:auth[_-]?(?:token|secret))/gi, /(?:access(?:[_-]?key)?|access[_-]?token)/gi, /(?:refresh[_-]?token)/gi, /(?:client[_-]?(?:id|secret))/gi, /(?:credential(?:s)?)/gi, // Certificates & keys - improved patterns /(?:private[_-]?(?:key|rsa|dsa|ecdsa|pem|pfx|pkcs12|p12))/gi, /(?:rsa[_-]?key)/gi, /(?:dsa[_-]?key)/gi, /(?:ecdsa[_-]?key)/gi, /(?:ssh[_-]?key)/gi, /(?:tls|ssl|x509)[_-]?(?:cert|certificate|key)/gi, /(?:cert(?:ificate)?|certfile)/gi, /(?:jwt.*)/gi, /(?:bearer.*)/gi, /(?:ca\.crt|ca\.key|ca\.pem|ca\.pfx|ca\.pkcs12|ca\.p12)/gi, // Session & cookie data /(?:session[_-]?id)/gi, /cookie/gi, // Database & connection strings /(?:db|database)[_-]?password/gi, /(?:connection[_-]?string|dsn)/gi, ]; private readonly sensitiveValuePatterns: RegExp[] = [ // JWT tokens (header.payload.signature) /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, // SSH keys (complete line including user@host) /ssh-(?:rsa|dss|ed25519|ecdsa)\s+[A-Za-z0-9+/=]+[^\r\n]*/gi, // API keys and tokens (common patterns) - improved to catch more variations /\b(?:api[_-]?key|token|secret|auth[_-]?key|access[_-]?key)\s*[:=]\s*['"]?[A-Za-z0-9+/=_-]{16,}['"]?/gi, // Secret-like strings with common prefixes (for test cases like "secret-key-...") /(?:secret|key|token|api)[_-][A-Za-z0-9_-]{15,}/gi, // Long alphanumeric strings that look like secrets (20+ chars) /(?.* { throw new Error('Create operation is not supported in read-only mode'); } /** * @throws {Error} This operation is not supported in read-only mode */ async update( _configMap: k8s.V1ConfigMap, _options?: ResourceOperationOptions, ): Promise { throw new Error('Update operation is not supported in read-only mode'); } /** * @throws {Error} This operation is not supported in read-only mode */ async patch( _name: string, _patch: unknown, _options?: ResourceOperationOptions, ): Promise { throw new Error('Patch operation is not supported in read-only mode'); } /** * @throws {Error} This operation is not supported in read-only mode */ async delete(_name: string, _options?: ResourceOperationOptions): Promise { throw new Error('Delete operation is not supported in read-only mode'); } /** * Get a ConfigMap by name */ async get(name: string, options?: ResourceOperationOptions): Promise { try { const namespace = options?.namespace || 'default'; let response = await this.client.core.readNamespacedConfigMap({ name, namespace, }); // Apply sanitization if requested or globally enforced if (!options?.skipSanitize || isSensitiveMaskEnabled()) { response = this.sanitizeConfigMapData(response); } return response; } catch (error) { this.handleApiError(error, 'Get', name); } } /** * List ConfigMaps with optional data sanitization */ async list(options?: ResourceOperationOptions): Promise { try { const namespace = options?.namespace; const listOptions = this.buildListOptions(options); let response; if (namespace) { response = await this.client.core.listNamespacedConfigMap({ namespace, pretty: listOptions.pretty, allowWatchBookmarks: listOptions.allowWatchBookmarks, _continue: listOptions.continue, fieldSelector: listOptions.fieldSelector, labelSelector: listOptions.labelSelector, limit: listOptions.limit, resourceVersion: listOptions.resourceVersion, resourceVersionMatch: listOptions.resourceVersionMatch, sendInitialEvents: listOptions.sendInitialEvents, timeoutSeconds: listOptions.timeoutSeconds, watch: listOptions.watch, }); } else { response = await this.client.core.listConfigMapForAllNamespaces({ allowWatchBookmarks: listOptions.allowWatchBookmarks, _continue: listOptions.continue, fieldSelector: listOptions.fieldSelector, labelSelector: listOptions.labelSelector, limit: listOptions.limit, pretty: listOptions.pretty, resourceVersion: listOptions.resourceVersion, resourceVersionMatch: listOptions.resourceVersionMatch, sendInitialEvents: listOptions.sendInitialEvents, timeoutSeconds: listOptions.timeoutSeconds, watch: listOptions.watch, }); } // Sanitize sensitive data if requested or globally enforced if (!options?.skipSanitize || isSensitiveMaskEnabled()) { response.items = response.items.map((item) => this.sanitizeConfigMapData(item)); } response.items = response.items.map((item) => ({ ...item, name: item.metadata?.name, namespace: item.metadata?.namespace, metadata: { labels: item.metadata?.labels, annotations: item.metadata?.annotations, creationTimestamp: item.metadata?.creationTimestamp, }, })); // Remove data from response if namespace is not provided if (!namespace) { response.items = response.items.map((item) => ({ ...item, creationTimestamp: item.metadata?.creationTimestamp, data: undefined, binaryData: undefined, metadata: undefined, })); } return response; } catch (error) { this.handleApiError(error, 'List'); } } /** * Watch ConfigMaps for changes */ watch(callback: WatchCallback, options?: ResourceOperationOptions): () => void { let stopWatching = false; let request: unknown = null; const startWatch = async (): Promise => { try { const namespace = options?.namespace; const listOptions = this.buildListOptions(options); const watch = new k8s.Watch(this.client.kubeConfig); request = await watch.watch( `/api/v1/${namespace ? `namespaces/${namespace}/` : ''}configmaps`, listOptions, (type: string, obj: k8s.V1ConfigMap) => { if (!stopWatching) { callback({ type: type as WatchEventType, object: obj, }); } }, (err: unknown) => { if (!stopWatching) { this.logger?.error(`Watch error for ConfigMaps: ${err}`); // For error events, we need to create a valid V1ConfigMap-like object // or handle the error differently since the callback expects V1ConfigMap const errorObj = { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'watch-error' }, data: { error: String(err) }, } as k8s.V1ConfigMap; callback({ type: WatchEventType.ERROR, object: errorObj, }); } }, ); } catch (error) { this.logger?.error(`Failed to start watch for ConfigMaps: ${error}`); throw error; } }; startWatch().catch((error) => { this.logger?.error(`Failed to start ConfigMap watch: ${error}`); }); return () => { stopWatching = true; if (request && typeof request === 'object' && request !== null && 'abort' in request) { (request as { abort: () => void }).abort(); } }; } /** * Get a value from a ConfigMap by key */ async getValue( name: string, key: string, options?: ResourceOperationOptions, ): Promise { const configMap = await this.get(name, options); return configMap.data?.[key]; } }