Files
local-image-editor-web/scripts/security-check.mjs
T

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.')