import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'; import { extname, join, relative, sep, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = dirname(dirname(fileURLToPath(import.meta.url))); const failures = []; const warnings = []; const normalize = value => value.split(sep).join('/').replace(/^\.\//, ''); const forbiddenPathPatterns = [ /(^|\/)node_modules(\/|$)/i, /(^|\/)runtime(\/|$)/i, /(^|\/)docker-data(\/|$)/i, /(^|\/)release-output(\/|$)/i, /(^|\/)operations\/(staging|updates|rollback|apply-[^/]+)(\/|$)/i, /(^|\/)operations\/(pending-operation|last-operation|first-admin-code)\.json$/i, /(^|\/)operations\/first-admin-code\.txt$/i, /(^|\/)(uploads|backups|logs|tmp|temp)(\/|$)/i, /^sessions(\/|$)/i, /(^|\/)updates(\/|$)/i, /^modules(\/|$)/i, /(^|\/)db\/.*\.(db|sqlite|sqlite3)(-wal|-shm)?$/i, /(^|\/)local-ai\/(comfyui|downloads|extract-temp)(\/|$)/i, /(^|\/)\.vendoo-install-mode$/i, /(^|\/)\.vendoo-github-state\.json$/i, ]; const allowedEnv = new Set(['.env.example', '.env.docker.example']); const binaryExt = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.zip', '.pdf', '.woff', '.woff2']); const secretPatterns = [ ['OpenAI-/kompatibler Schlüssel', /\bsk-[A-Za-z0-9_-]{20,}\b/g], ['GitHub Classic Token', /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g], ['GitHub Fine-grained Token', /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g], ['Slack Token', /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g], ['Private Key', /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g], ['AWS Access Key', /\bAKIA[0-9A-Z]{16}\b/g], ]; function walk(dir, out = []) { for (const name of readdirSync(dir)) { if (['.git', 'node_modules', 'release-output', 'archiv', 'archive', 'archives'].includes(name.toLowerCase())) continue; const abs = join(dir, name); const rel = normalize(relative(root, abs)); const stat = lstatSync(abs); if (stat.isSymbolicLink()) { warnings.push(`Symlink wird nicht geprüft oder gepackt: ${rel}`); continue; } if (stat.isDirectory()) walk(abs, out); else out.push(rel); } return out; } // Absichtlich immer den kompletten Arbeitsbaum prüfen. Ein Git-Erstimport enthält // viele noch unversionierte Dateien; "git ls-files" würde diese nicht erfassen. const files = walk(root); for (const rel of files) { const envLike = /(^|\/)\.env(?:\..+)?$/i.test(rel); if (envLike && !allowedEnv.has(rel)) failures.push(`Verbotene Environment-Datei: ${rel}`); if (!allowedEnv.has(rel) && forbiddenPathPatterns.some(pattern => pattern.test(rel))) { failures.push(`Verbotene Laufzeit-/Secret-Datei: ${rel}`); } const abs = join(root, rel); if (!existsSync(abs)) continue; const stat = lstatSync(abs); if (stat.size > 50 * 1024 * 1024) failures.push(`Datei größer als 50 MiB: ${rel}`); if (binaryExt.has(extname(rel).toLowerCase()) || stat.size > 5 * 1024 * 1024) continue; let text; try { text = readFileSync(abs, 'utf8'); } catch { continue; } for (const [label, pattern] of secretPatterns) { pattern.lastIndex = 0; if (pattern.test(text)) failures.push(`${label} in ${rel}`); } if (!allowedEnv.has(rel)) { const envAssignment = /^(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|OPENROUTER_API_KEY|ETSY_API_KEY|EBAY_CLIENT_SECRET|SMTP_PASS|VENDOO_SETUP_TOKEN|LOCAL_IMAGE_TOKEN)=(.+)$/gm; for (const match of text.matchAll(envAssignment)) { const value = String(match[1] || '').trim(); if (value && !/^\$\{|^<|^CHANGEME$/i.test(value)) failures.push(`Befülltes Secret-Feld in ${rel}: ${match[0].split('=')[0]}`); } } } const gitignorePath = join(root, '.gitignore'); if (!existsSync(gitignorePath)) { failures.push('.gitignore fehlt.'); } else { const gitignore = readFileSync(gitignorePath, 'utf8'); for (const marker of ['.env', 'node_modules/', 'runtime/', 'modules/', '/Archiv/', '/archive/', '/archives/', 'operations/pending-operation.json', 'local-ai/comfyui/', '.vendoo-install-mode']) { if (!gitignore.includes(marker)) failures.push(`.gitignore enthält nicht: ${marker}`); } } const uniqueFailures = [...new Set(failures)]; if (uniqueFailures.length) { console.error(`Git-Sicherheitsgate fehlgeschlagen (${uniqueFailures.length}):`); uniqueFailures.slice(0, 100).forEach(item => console.error(`- ${item}`)); if (uniqueFailures.length > 100) console.error(`- ... ${uniqueFailures.length - 100} weitere Treffer`); process.exit(1); } console.log(`Git-Sicherheitsgate erfolgreich: ${files.length} Dateien geprüft; keine Secrets oder Laufzeitdaten gefunden.`); warnings.forEach(item => console.warn(`[WARN] ${item}`));