From d402cf3f783a3ba0ceb33e5a6cec449880ca8e2e Mon Sep 17 00:00:00 2001 From: rayanahmax Date: Tue, 21 Jul 2026 11:42:55 +0530 Subject: [PATCH 1/2] feat: add k8s secret encoder decoder and manifest sanitizer tool page --- astro.config.mjs | 11 +- public/llms.txt | 1 + public/robots.txt | 13 +- src/components/shared/Navbar.astro | 1 + src/components/tools/K8sManifestSanitizer.tsx | 423 ++++++++++++++ src/pages/index.astro | 8 +- src/pages/k8s-manifest-sanitizer.astro | 536 ++++++++++++++++++ 7 files changed, 988 insertions(+), 5 deletions(-) create mode 100644 src/components/tools/K8sManifestSanitizer.tsx create mode 100644 src/pages/k8s-manifest-sanitizer.astro diff --git a/astro.config.mjs b/astro.config.mjs index a5fa9a4..5178a41 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -32,11 +32,13 @@ export default defineConfig({ integrations: [ react(), sitemap({ - // Exclude legal/utility pages — not useful for search indexers + // Exclude legal/utility pages and error pages — not useful for search indexers filter: (page) => !page.includes('/terms') && !page.includes('/privacy') && - !page.includes('/contact'), + !page.includes('/contact') && + !page.includes('/404') && + !page.includes('/500'), // Priority tiers: hero tools at 1.0, all others at 0.9 customPages: [], @@ -44,13 +46,16 @@ export default defineConfig({ const heroTools = [ 'https://configdev.com/cidr-subnet-calculator', 'https://configdev.com/pii-masker-log-scrubber', + 'https://configdev.com/k8s-manifest-sanitizer', 'https://configdev.com/env-to-json', 'https://configdev.com/json-to-env', + 'https://configdev.com/cron-to-systemd-converter', + 'https://configdev.com/csv-to-json-schema', ]; const isHero = heroTools.some((url) => item.url === url || item.url === url + '/'); return { ...item, - priority: isHero ? 1.0 : 0.9, + priority: isHero ? 1.0 : 0.8, changefreq: isHero ? EnumChangefreq.WEEKLY : EnumChangefreq.MONTHLY, }; }, diff --git a/public/llms.txt b/public/llms.txt index a7c52c8..63cc0dd 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -10,6 +10,7 @@ ConfigDev is a high-performance web utility suite designed for systems administr - [Env to JSON Converter](https://configdev.com/env-to-json): Parse standard key-value `.env` files into clean structured JSON configuration objects with type-casting. - [JSON to Env Converter](https://configdev.com/json-to-env): Flatten nested JSON objects into standard flat `.env` key-value lines with custom delimiters. - [PII Masker & Log Scrubber](https://configdev.com/pii-masker-log-scrubber): Redact emails, secrets, tokens, and IP strings out of raw log dumps locally. +- [Kubernetes Secret Encoder/Decoder & Manifest Sanitizer](https://configdev.com/k8s-manifest-sanitizer): Base64 encode/decode Kubernetes Secret data blocks and sanitize manifests for safe Git commits — 100% client-side using native btoa/atob. - [CIDR Subnet Calculator](https://configdev.com/cidr-subnet-calculator): Compute subnet boundaries, usable host ranges, broadcast addresses, and masks for IPv4. - [CSV to JSON Schema Builder](https://configdev.com/csv-to-json-schema): Inspect CSV columns and auto-infer valid JSON Schema Draft-07 specification files. diff --git a/public/robots.txt b/public/robots.txt index c6b6f12..386db53 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,8 +1,19 @@ User-agent: * Allow: / -# Block error pages from indexing +# Block error pages — no indexing value Disallow: /404 +Disallow: /404.html Disallow: /500 +Disallow: /500.html +# Block legal/utility pages from wasting crawl budget +Disallow: /terms +Disallow: /terms.html +Disallow: /privacy +Disallow: /privacy.html +Disallow: /contact +Disallow: /contact.html + +# Sitemap location Sitemap: https://configdev.com/sitemap-index.xml diff --git a/src/components/shared/Navbar.astro b/src/components/shared/Navbar.astro index 27f905a..ef30b7e 100644 --- a/src/components/shared/Navbar.astro +++ b/src/components/shared/Navbar.astro @@ -13,6 +13,7 @@ const toolCategories = [ label: 'Security & DevSecOps', tools: [ { href: '/pii-masker-log-scrubber', label: 'PII Masker & Scrubber' }, + { href: '/k8s-manifest-sanitizer', label: 'K8s Manifest Sanitizer' }, ] }, { diff --git a/src/components/tools/K8sManifestSanitizer.tsx b/src/components/tools/K8sManifestSanitizer.tsx new file mode 100644 index 0000000..dd260a1 --- /dev/null +++ b/src/components/tools/K8sManifestSanitizer.tsx @@ -0,0 +1,423 @@ +import { useState, useEffect, useId, type DragEvent } from 'react'; + +interface K8sStats { + totalSecrets: number; + decodedFields: number; + encodedFields: number; + sanitizedFields: number; +} + +type ToolMode = 'decode' | 'encode' | 'sanitize'; + +export default function K8sManifestSanitizer() { + const [inputYaml, setInputYaml] = useState(''); + const [outputYaml, setOutputYaml] = useState(''); + const [mode, setMode] = useState('decode'); + const [copied, setCopied] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [stats, setStats] = useState({ + totalSecrets: 0, + decodedFields: 0, + encodedFields: 0, + sanitizedFields: 0, + }); + + const inputId = useId(); + const modeSelectId = useId(); + + // Safe Base64 decode with UTF-8 support + const safeBase64Decode = (str: string): string => { + try { + // Handle both standard base64 and URL-safe base64 + const normalized = str.replace(/-/g, '+').replace(/_/g, '/'); + const decoded = atob(normalized); + // Convert to UTF-8 + return decodeURIComponent( + decoded + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join('') + ); + } catch { + return str; // Return original if decode fails + } + }; + + // Safe Base64 encode with UTF-8 support + const safeBase64Encode = (str: string): string => { + try { + // Convert UTF-8 to percent-encoding, then to base64 + const encoded = encodeURIComponent(str) + .replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))); + return btoa(encoded); + } catch { + return str; // Return original if encode fails + } + }; + + // Parse and transform Kubernetes Secret YAML + const processK8sYaml = (yaml: string, processingMode: ToolMode): { output: string; stats: K8sStats } => { + const stats: K8sStats = { + totalSecrets: 0, + decodedFields: 0, + encodedFields: 0, + sanitizedFields: 0, + }; + + if (!yaml.trim()) { + return { output: '', stats }; + } + + // Split multi-document YAML (separated by ---) + const documents = yaml.split(/^---$/m).filter((doc) => doc.trim()); + const processedDocs: string[] = []; + + documents.forEach((doc) => { + // Check if this is a Secret resource + const isSecret = /kind:\s*Secret/i.test(doc); + if (!isSecret) { + processedDocs.push(doc); + return; + } + + stats.totalSecrets++; + + let processedDoc = doc; + + // Process data: block (base64 encoded values) + const dataBlockMatch = processedDoc.match(/^([ \t]*data:[ \t]*\r?\n)((?:[ \t]+.+\r?\n)*)/m); + if (dataBlockMatch) { + const indent = dataBlockMatch[1]; + const dataContent = dataBlockMatch[2]; + const lines = dataContent.split('\n'); + const processedLines: string[] = []; + + lines.forEach((line) => { + const keyValueMatch = line.match(/^([ \t]+)([a-zA-Z0-9._-]+):\s*(.+)$/); + if (keyValueMatch) { + const lineIndent = keyValueMatch[1]; + const key = keyValueMatch[2]; + const value = keyValueMatch[3].trim(); + + let newValue = value; + + if (processingMode === 'decode') { + // Decode base64 to plain text + newValue = safeBase64Decode(value); + if (newValue !== value) stats.decodedFields++; + } else if (processingMode === 'encode') { + // Encode plain text to base64 + newValue = safeBase64Encode(value); + if (newValue !== value) stats.encodedFields++; + } else if (processingMode === 'sanitize') { + // Replace with redacted placeholder + newValue = ''; + stats.sanitizedFields++; + } + + processedLines.push(`${lineIndent}${key}: ${newValue}`); + } else if (line.trim()) { + processedLines.push(line); + } + }); + + processedDoc = processedDoc.replace( + dataBlockMatch[0], + indent + processedLines.join('\n') + '\n' + ); + } + + // Process stringData: block (plain text values) + const stringDataBlockMatch = processedDoc.match(/^([ \t]*stringData:[ \t]*\r?\n)((?:[ \t]+.+\r?\n)*)/m); + if (stringDataBlockMatch) { + const indent = stringDataBlockMatch[1]; + const stringDataContent = stringDataBlockMatch[2]; + const lines = stringDataContent.split('\n'); + const processedLines: string[] = []; + + lines.forEach((line) => { + const keyValueMatch = line.match(/^([ \t]+)([a-zA-Z0-9._-]+):\s*(.+)$/); + if (keyValueMatch) { + const lineIndent = keyValueMatch[1]; + const key = keyValueMatch[2]; + const value = keyValueMatch[3].trim(); + + let newValue = value; + + if (processingMode === 'encode') { + // Encode plain text to base64 and move to data: block + newValue = safeBase64Encode(value); + stats.encodedFields++; + processedLines.push(`${lineIndent}${key}: ${newValue}`); + } else if (processingMode === 'sanitize') { + // Replace with redacted placeholder + newValue = ''; + stats.sanitizedFields++; + processedLines.push(`${lineIndent}${key}: ${newValue}`); + } else { + // decode mode: leave stringData as-is + processedLines.push(line); + } + } else if (line.trim()) { + processedLines.push(line); + } + }); + + if (processingMode === 'encode') { + // Convert stringData: to data: when encoding + processedDoc = processedDoc.replace( + stringDataBlockMatch[0], + indent.replace('stringData', 'data') + processedLines.join('\n') + '\n' + ); + } else { + processedDoc = processedDoc.replace( + stringDataBlockMatch[0], + indent + processedLines.join('\n') + '\n' + ); + } + } + + processedDocs.push(processedDoc); + }); + + return { + output: processedDocs.join('---\n'), + stats, + }; + }; + + useEffect(() => { + const result = processK8sYaml(inputYaml, mode); + setOutputYaml(result.output); + setStats(result.stats); + }, [inputYaml, mode]); + + // Drag-and-drop file handling + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = () => { + setIsDragging(false); + }; + + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const droppedFile = e.dataTransfer.files[0]; + if (droppedFile && (droppedFile.name.endsWith('.yaml') || droppedFile.name.endsWith('.yml'))) { + const reader = new FileReader(); + reader.onload = (event) => { + if (event.target?.result) { + setInputYaml(event.target.result as string); + } + }; + reader.readAsText(droppedFile); + } + }; + + const handleLoadSample = () => { + const sampleYaml = `apiVersion: v1 +kind: Secret +metadata: + name: db-credentials + namespace: production +type: Opaque +data: + username: YWRtaW4= + password: UEBzc3cwcmQxMjMh + database-url: cG9zdGdyZXNxbDovL2FkbWluOlBAc3N3MHJkMTIzIUBkYi5leGFtcGxlLmNvbTo1NDMyL215ZGI= +--- +apiVersion: v1 +kind: Secret +metadata: + name: api-keys +type: Opaque +stringData: + stripe-key: sk_live_abc123xyz789 + sendgrid-api-key: SG.1234567890abcdefghijklmnop + jwt-secret: my-super-secret-jwt-signing-key`; + setInputYaml(sampleYaml); + }; + + const handleCopy = async () => { + if (!outputYaml) return; + try { + await navigator.clipboard.writeText(outputYaml); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Clipboard access fallback + } + }; + + const handleDownload = () => { + if (!outputYaml) return; + const blob = new Blob([outputYaml], { type: 'text/yaml' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = mode === 'sanitize' ? 'secret.sanitized.yaml' : 'secret.yaml'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + return ( +
+ {/* Mode Selector Panel */} +
+
+ +
+ {[ + { value: 'decode', label: 'Decode Base64 → Plain Text', desc: 'View secrets in readable format' }, + { value: 'encode', label: 'Encode Plain Text → Base64', desc: 'Convert stringData to data format' }, + { value: 'sanitize', label: 'Sanitize for Git', desc: 'Replace secrets with ' }, + ].map((option) => ( + + ))} +
+
+
+ + {/* Editor Space */} +
+ {/* Input YAML Box */} +
+
+ + Input YAML / Drop Files + + +
+
+