77 lines
3.2 KiB
JavaScript
77 lines
3.2 KiB
JavaScript
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.')
|