import { execFileSync } from 'node:child_process' import { existsSync, readFileSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' const root = fileURLToPath(new URL('../', import.meta.url)) const problems = [] const envExample = readFileSync(join(root, '.env.example'), 'utf8') for (const line of envExample.split('\n')) { const match = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim()) if (!match) continue const [, key, value] = match const isSecretKey = /(SECRET|TOKEN|PASSWORD|PRIVATE_KEY|API_KEY)/.test(key) const isPlaceholder = !value || /^(?:change[-_]?me|example|placeholder|your[-_].*|<.*>)$/i.test(value) if (isSecretKey && !isPlaceholder) { problems.push(`.env.example must not contain a real-looking value for ${key}`) } if (key.startsWith('VITE_') && /(SECRET|TOKEN|PASSWORD|KEY)/.test(key)) { problems.push(`${key} would be public because VITE_* values are bundled into browser code`) } if (/-----BEGIN .*PRIVATE KEY-----/.test(value)) { problems.push(`.env.example contains private-key material in ${key}`) } } const dockerfile = readFileSync(join(root, 'Dockerfile'), 'utf8') if (/COPY\s+[^\n]*\.env(?:\s|$)/i.test(dockerfile)) { problems.push('Dockerfile must not copy .env files explicitly') } const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) const fabricVersion = packageJson.dependencies?.fabric ?? '' const fabricMatch = /^[~^]?(\d+)\.(\d+)\.(\d+)/.exec(fabricVersion) const fabricMajor = Number(fabricMatch?.[1] ?? 0) const fabricMinor = Number(fabricMatch?.[2] ?? 0) if (!fabricMatch || fabricMajor < 7 || (fabricMajor === 7 && fabricMinor < 4)) { problems.push(`Fabric must be at least 7.4.0; found ${fabricVersion || 'missing'}`) } if (existsSync(join(root, '.git'))) { try { const trackedEnvFiles = execFileSync('git', ['ls-files', '.env', '.env.*'], { cwd: root, encoding: 'utf8', }).trim().split('\n').filter(Boolean).filter((file) => file !== '.env.example') if (trackedEnvFiles.length) problems.push(`Tracked environment files: ${trackedEnvFiles.join(', ')}`) const credentialPattern = '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|AIza[0-9A-Za-z_-]{20,}' const grep = execFileSync( 'git', ['grep', '-lEI', '--', credentialPattern], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }, ).trim() if (grep) problems.push(`Potential credential material found in tracked files: ${grep.split('\n').join(', ')}`) const history = execFileSync( 'git', ['log', '--all', '-p', '--no-ext-diff', '--no-textconv', '--no-color', '--format='], { cwd: root, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 }, ) if (new RegExp(credentialPattern, 'i').test(history)) { problems.push('Potential credential material exists in Git history') } } catch (error) { // git grep returns exit code 1 when there are no matches, which is expected. if (error?.status !== 1) problems.push('Unable to complete Git secret scan') } } if (problems.length) { console.error(problems.join('\n')) process.exit(1) } console.log('Security checks passed.')