diff --git a/README.md b/README.md index 2321409..66c0035 100644 --- a/README.md +++ b/README.md @@ -2,43 +2,142 @@ **[https://configdev.com/](https://configdev.com/)** -**Modern, client-side developer utilities for DevOps and backend engineers.** +**Modern, client-side developer utilities for DevOps engineers, cloud architects, and SREs.** -ConfigDev is a suite of zero-latency, privacy-first tools designed to help developers solve everyday problems without compromising data security or infrastructure integrity. +ConfigDev is a suite of zero-latency, privacy-first tools that solve everyday infrastructure problems without compromising data security. All processing runs 100% inside your browser โ€” no uploads, no server round-trips, no logs. -## ๐Ÿ›ก๏ธ The Local-First, Privacy-First Philosophy +--- -In modern engineering, pasting sensitive data (like stack traces, configuration files, and API logs) into random online utilities or generative AI interfaces is a massive security risk. +## The Local-First, Privacy-First Philosophy -ConfigDev solves this by adhering strictly to a **Local-First, Privacy-First** philosophy: -- **Zero Ingress**: Data never leaves your machine. All parsing, formatting, and scrubbing happens natively inside your browser's local sandbox (V8 runtime). -- **No Backend Databases**: We don't store your history, logs, or configurations. -- **100% Network Isolation**: Once the app loads, you can disable your internet connection, and the tools will continue to function perfectly. +Pasting sensitive data โ€” stack traces, Kubernetes manifests, API keys, server logs โ€” into random online utilities is a real security risk. Most tools process your input on a backend server, meaning your production secrets transit infrastructure you don't control. -## ๐Ÿš€ Hero Tools +ConfigDev eliminates this entirely: -- [**PII Masker & Log Scrubber**](https://configdev.com/pii-masker-log-scrubber): A powerful regex-driven engine that strips sensitive PII (emails, IPs, JWTs, Secrets, Credit Cards) out of raw logs locally so you can safely share them with LLMs. -- [**CIDR Subnet Calculator**](https://configdev.com/cidr-subnet-calculator): A fast, visual subnet mask calculator for DevOps and networking engineers to plan IP ranges locally without internet connection. -- [**Cron to Systemd Converter**](https://configdev.com/cron-to-systemd-converter): Easily translate legacy crontab expressions into modern Systemd timer units and service files. -- [**CSV to JSON Schema**](https://configdev.com/csv-to-json-schema): Instantly convert flat CSV data into nested JSON schemas directly in your browser. -- [**ENV to JSON**](https://configdev.com/env-to-json): Convert flat `.env` variable lists into structured JSON objects. -- [**JSON to ENV**](https://configdev.com/json-to-env): Transform nested or flat JSON objects back into flattened `.env` variable syntax. +- **Zero Ingress** โ€” Data never leaves your machine. All parsing, encoding, and scrubbing runs inside your browser's V8 sandbox. +- **No Backend** โ€” No databases, no history, no analytics on your input. +- **Offline Capable** โ€” Once the page loads, disconnect from the internet. Every tool keeps working. +- **Verifiable** โ€” Open DevTools โ†’ Network tab while using any tool. Zero outbound requests. -## ๐Ÿ’ป Development +--- -This project is built with [Astro](https://astro.build/) for exceptional performance and zero-JS static rendering by default. +## Tools -### Setup +### Security & DevSecOps + +| Tool | URL | Description | +|---|---|---| +| PII Masker & Log Scrubber | [/pii-masker-log-scrubber](https://configdev.com/pii-masker-log-scrubber) | Strip emails, IPs, JWTs, credit cards, and secrets from raw logs locally before sharing with LLMs or issue trackers | +| K8s Manifest Sanitizer | [/k8s-manifest-sanitizer](https://configdev.com/k8s-manifest-sanitizer) | Base64 encode/decode Kubernetes Secret data blocks, convert stringData to data, and scrub manifests for safe Git commits | + +### Infrastructure & Config + +| Tool | URL | Description | +|---|---|---| +| Crontab to Systemd Converter | [/cron-to-systemd-converter](https://configdev.com/cron-to-systemd-converter) | Translate legacy crontab expressions into production-ready systemd `.service` and `.timer` unit files | +| Env to JSON Converter | [/env-to-json](https://configdev.com/env-to-json) | Parse standard `.env` key-value files into clean, formatted JSON objects ready for any pipeline | +| JSON to Env Converter | [/json-to-env](https://configdev.com/json-to-env) | Flatten nested JSON objects into standard `.env` key-value lines with configurable delimiter formatting | + +### Networking & Data + +| Tool | URL | Description | +|---|---|---| +| CIDR Subnet Calculator | [/cidr-subnet-calculator](https://configdev.com/cidr-subnet-calculator) | Compute subnet boundaries, usable host ranges, broadcast addresses, and network masks for any IPv4 block | +| CSV to JSON Schema Builder | [/csv-to-json-schema](https://configdev.com/csv-to-json-schema) | Inspect CSV column structures and auto-generate a valid JSON Schema Draft-07 specification file | + +--- + +## Tech Stack + +| Layer | Technology | +|---|---| +| Framework | [Astro](https://astro.build/) v7 โ€” static output, zero-JS by default | +| UI Components | React 19 (islands, `client:load`) | +| Styling | Tailwind CSS v4 | +| Deployment | Cloudflare Pages | +| Sitemap | `@astrojs/sitemap` | + +--- + +## Development ```bash +# Install dependencies npm install + +# Start dev server npm run dev + +# Type-check and lint +npm run lint + +# Production build +npm run build ``` -### Deployment +The project uses flat file routing (`build.format: 'file'`), so pages emit as `/page-name.html` rather than `/page-name/index.html`. This eliminates the trailing-slash duplicate-URL issue in Google Search Console. + +--- + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ shared/ +โ”‚ โ”‚ โ”œโ”€โ”€ Navbar.astro # Site-wide navigation with grouped tool categories +โ”‚ โ”‚ โ”œโ”€โ”€ Footer.astro +โ”‚ โ”‚ โ””โ”€โ”€ RelatedTools.astro # Cross-linking widget used on all tool pages +โ”‚ โ””โ”€โ”€ tools/ +โ”‚ โ”œโ”€โ”€ CIDRCalculator.tsx +โ”‚ โ”œโ”€โ”€ CronToSystemd.tsx +โ”‚ โ”œโ”€โ”€ CsvToSchema.tsx +โ”‚ โ”œโ”€โ”€ EnvToJson.tsx +โ”‚ โ”œโ”€โ”€ JsonToEnv.tsx +โ”‚ โ”œโ”€โ”€ K8sManifestSanitizer.tsx +โ”‚ โ””โ”€โ”€ PiiScrubber.tsx +โ”œโ”€โ”€ layouts/ +โ”‚ โ””โ”€โ”€ Layout.astro # Base HTML shell, meta, canonical, JSON-LD +โ”œโ”€โ”€ pages/ +โ”‚ โ”œโ”€โ”€ index.astro # Homepage with category grid and search/filter +โ”‚ โ”œโ”€โ”€ k8s-manifest-sanitizer.astro +โ”‚ โ”œโ”€โ”€ pii-masker-log-scrubber.astro +โ”‚ โ”œโ”€โ”€ cidr-subnet-calculator.astro +โ”‚ โ”œโ”€โ”€ cron-to-systemd-converter.astro +โ”‚ โ”œโ”€โ”€ csv-to-json-schema.astro +โ”‚ โ”œโ”€โ”€ env-to-json.astro +โ”‚ โ”œโ”€โ”€ json-to-env.astro +โ”‚ โ””โ”€โ”€ ... +โ””โ”€โ”€ styles/ + โ””โ”€โ”€ global.css +public/ +โ”œโ”€โ”€ robots.txt # Crawl directives โ€” error/legal pages blocked +โ”œโ”€โ”€ llms.txt # LLM-readable site manifest +โ””โ”€โ”€ _redirects # Cloudflare Pages redirect rules +astro.config.mjs # Site config, sitemap priorities, build format +``` + +--- + +## Deployment + +Configured for Cloudflare Pages. Set environment variables in your Pages dashboard before deploying: + +| Variable | Purpose | +|---|---| +| `PUBLIC_GA_ID` | Google Analytics measurement ID (optional) | +| `PUBLIC_YANDEX_VERIFICATION` | Yandex Webmaster verification (optional) | + +Deploy command: `npm run build` โ†’ output directory: `dist/` + +--- + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. -ConfigDev is configured for seamless deployment to Cloudflare Pages. Make sure to define your environment variables (like `PUBLIC_GA_ID`) in your platform settings before deploying! +--- -## ๐Ÿ“œ License +## License -This project is open-source and licensed under the [MIT License](LICENSE). +MIT โ€” see [LICENSE](LICENSE). 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 + + +
+
+