mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
326 lines
9.8 KiB
TypeScript
326 lines
9.8 KiB
TypeScript
import { execSync } from 'node:child_process'
|
|
import * as fs from 'node:fs'
|
|
import * as path from 'node:path'
|
|
import * as readline from 'node:readline'
|
|
|
|
interface Advisory {
|
|
id: number
|
|
module_name: string
|
|
severity: string
|
|
title: string
|
|
vulnerable_versions: string
|
|
patched_versions: string
|
|
findings: Array<{
|
|
version: string
|
|
paths: string[]
|
|
}>
|
|
}
|
|
|
|
interface AuditOutput {
|
|
advisories: Record<string, Advisory>
|
|
metadata: {
|
|
vulnerabilities: Record<string, number>
|
|
dependencies: number
|
|
totalDependencies: number
|
|
}
|
|
}
|
|
|
|
interface VulnerableModule {
|
|
module_name: string
|
|
advisories: Advisory[]
|
|
highestSeverity: string
|
|
overrideVersion: string
|
|
allPaths: string[]
|
|
}
|
|
|
|
const WORKSPACE_YAML_PATH = path.join(process.cwd(), 'pnpm-workspace.yaml')
|
|
const LOCKFILE_PATH = path.join(process.cwd(), 'pnpm-lock.yaml')
|
|
|
|
const SEVERITY_ORDER = ['critical', 'high', 'moderate', 'low']
|
|
|
|
function runAudit(): AuditOutput {
|
|
try {
|
|
const stdout = execSync('pnpm audit --json', {
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
})
|
|
return JSON.parse(stdout)
|
|
} catch (error: any) {
|
|
// pnpm audit exits with code 1 when vulnerabilities exist
|
|
if (error.stdout) {
|
|
return JSON.parse(error.stdout)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
function parseMinVersion(patchedVersions: string): string | null {
|
|
const match = patchedVersions.match(/>=(\d+\.\d+\.\d+)/)
|
|
return match ? match[1] : null
|
|
}
|
|
|
|
function compareSemver(a: string, b: string): number {
|
|
const pa = a.split('.').map(Number)
|
|
const pb = b.split('.').map(Number)
|
|
for (let i = 0; i < 3; i++) {
|
|
if (pa[i] !== pb[i]) return pa[i] - pb[i]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function groupAdvisories(advisories: Record<string, Advisory>): VulnerableModule[] {
|
|
const byModule = new Map<string, Advisory[]>()
|
|
|
|
for (const adv of Object.values(advisories)) {
|
|
if (adv.patched_versions === '<0.0.0') continue
|
|
|
|
const existing = byModule.get(adv.module_name) ?? []
|
|
existing.push(adv)
|
|
byModule.set(adv.module_name, existing)
|
|
}
|
|
|
|
const result: VulnerableModule[] = []
|
|
|
|
for (const [module_name, advs] of byModule) {
|
|
const allPaths = [...new Set(advs.flatMap((a) => a.findings.flatMap((f) => f.paths)))]
|
|
|
|
const versions = advs
|
|
.map((a) => parseMinVersion(a.patched_versions))
|
|
.filter(Boolean) as string[]
|
|
const highestVersion = versions.sort(compareSemver).pop()!
|
|
const overrideVersion = `^${highestVersion}`
|
|
|
|
const highestSeverity = advs
|
|
.map((a) => a.severity)
|
|
.sort((a, b) => SEVERITY_ORDER.indexOf(a) - SEVERITY_ORDER.indexOf(b))
|
|
.at(0)!
|
|
|
|
result.push({
|
|
module_name,
|
|
advisories: advs,
|
|
highestSeverity,
|
|
overrideVersion,
|
|
allPaths,
|
|
})
|
|
}
|
|
|
|
result.sort((a, b) => {
|
|
const sevDiff =
|
|
SEVERITY_ORDER.indexOf(a.highestSeverity) - SEVERITY_ORDER.indexOf(b.highestSeverity)
|
|
if (sevDiff !== 0) return sevDiff
|
|
return a.module_name.localeCompare(b.module_name)
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
function displayVulnerabilities(modules: VulnerableModule[]): void {
|
|
console.log('\nVulnerable dependencies (patchable):\n')
|
|
|
|
const severityColors: Record<string, string> = {
|
|
critical: '\x1b[31m',
|
|
high: '\x1b[33m',
|
|
moderate: '\x1b[36m',
|
|
low: '\x1b[37m',
|
|
}
|
|
const reset = '\x1b[0m'
|
|
|
|
for (let i = 0; i < modules.length; i++) {
|
|
const m = modules[i]
|
|
const color = severityColors[m.highestSeverity] ?? reset
|
|
|
|
console.log(
|
|
` ${String(i + 1).padStart(2)}. ${color}[${m.highestSeverity.toUpperCase()}]${reset} ` +
|
|
`${m.module_name} -> ${m.overrideVersion}`
|
|
)
|
|
|
|
const maxPaths = 3
|
|
const paths = m.allPaths.slice(0, maxPaths)
|
|
for (const p of paths) {
|
|
console.log(` via ${p.replace(/__/g, '/')}`)
|
|
}
|
|
if (m.allPaths.length > maxPaths) {
|
|
console.log(` ... and ${m.allPaths.length - maxPaths} more`)
|
|
}
|
|
}
|
|
|
|
console.log('')
|
|
}
|
|
|
|
function promptSelection(modules: VulnerableModule[]): Promise<VulnerableModule> {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
})
|
|
|
|
return new Promise((resolve, reject) => {
|
|
rl.question(`Select vulnerability to fix (1-${modules.length}): `, (answer) => {
|
|
rl.close()
|
|
const num = parseInt(answer, 10)
|
|
if (isNaN(num) || num < 1 || num > modules.length) {
|
|
reject(new Error(`Invalid selection: ${answer}`))
|
|
return
|
|
}
|
|
resolve(modules[num - 1])
|
|
})
|
|
})
|
|
}
|
|
|
|
function escapeRegex(str: string): string {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
}
|
|
|
|
function formatOverrideLine(moduleName: string, version: string): string {
|
|
const key = moduleName.includes('@') ? `'${moduleName}'` : moduleName
|
|
return ` ${key}: ${version}`
|
|
}
|
|
|
|
function addOverride(yamlContent: string, moduleName: string, version: string): string {
|
|
const lines = yamlContent.split('\n')
|
|
|
|
const overridesIdx = lines.findIndex((line) => /^overrides:\s*$/.test(line))
|
|
if (overridesIdx === -1) {
|
|
throw new Error('Could not find "overrides:" section in pnpm-workspace.yaml')
|
|
}
|
|
|
|
// Find the end of the overrides block (next non-indented, non-empty line)
|
|
let blockEnd = overridesIdx + 1
|
|
while (blockEnd < lines.length) {
|
|
const line = lines[blockEnd]
|
|
if (line === '' || /^\s+/.test(line)) {
|
|
blockEnd++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Check if this module already has an override
|
|
const existingPattern = new RegExp(`^\\s+['"]?${escapeRegex(moduleName)}['"]?\\s*:`)
|
|
const existingIdx = lines.findIndex(
|
|
(line, idx) => idx > overridesIdx && idx < blockEnd && existingPattern.test(line)
|
|
)
|
|
|
|
if (existingIdx !== -1) {
|
|
console.log(`\nWARNING: Override for "${moduleName}" already exists:`)
|
|
console.log(` ${lines[existingIdx].trim()}`)
|
|
console.log(` Replacing with: ${moduleName}: ${version}`)
|
|
lines[existingIdx] = formatOverrideLine(moduleName, version)
|
|
return lines.join('\n')
|
|
}
|
|
|
|
// Insert new override at the end of the overrides block
|
|
const newLine = formatOverrideLine(moduleName, version)
|
|
lines.splice(blockEnd, 0, newLine)
|
|
return lines.join('\n')
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
console.log('Running pnpm audit...')
|
|
const auditResult = runAudit()
|
|
|
|
const modules = groupAdvisories(auditResult.advisories)
|
|
|
|
if (modules.length === 0) {
|
|
console.log('No patchable vulnerabilities found.')
|
|
process.exit(0)
|
|
}
|
|
|
|
displayVulnerabilities(modules)
|
|
|
|
const selected = await promptSelection(modules)
|
|
|
|
// Snapshot original files for revert on failure
|
|
const originalYaml = fs.readFileSync(WORKSPACE_YAML_PATH, 'utf-8')
|
|
const originalLockfile = fs.readFileSync(LOCKFILE_PATH, 'utf-8')
|
|
|
|
function revert(): void {
|
|
console.log('\nReverting pnpm-workspace.yaml and pnpm-lock.yaml...')
|
|
fs.writeFileSync(WORKSPACE_YAML_PATH, originalYaml, 'utf-8')
|
|
fs.writeFileSync(LOCKFILE_PATH, originalLockfile, 'utf-8')
|
|
console.log('Reverted to original state.')
|
|
}
|
|
|
|
console.log(`\nAdding override: ${selected.module_name}: ${selected.overrideVersion}`)
|
|
const updatedYaml = addOverride(originalYaml, selected.module_name, selected.overrideVersion)
|
|
fs.writeFileSync(WORKSPACE_YAML_PATH, updatedYaml, 'utf-8')
|
|
console.log('Updated pnpm-workspace.yaml')
|
|
|
|
console.log('\nRunning pnpm install (with override)...')
|
|
try {
|
|
execSync('pnpm install', {
|
|
stdio: 'pipe',
|
|
encoding: 'utf-8',
|
|
})
|
|
} catch (error: any) {
|
|
const output = (error.stdout ?? '') + (error.stderr ?? '')
|
|
if (output.includes('ERR_PNPM_NO_MATCHING_VERSION')) {
|
|
console.error(
|
|
`\nNo matching version found for "${selected.module_name}@${selected.overrideVersion}", the minimumReleaseAge option forbids it from installing.`
|
|
)
|
|
|
|
// Extract and display dependency chains that failed due to minimumReleaseAge
|
|
const blocks = output.split('ERR_PNPM_NO_MATCHING_VERSION')
|
|
for (const block of blocks.slice(1)) {
|
|
const versionMatch = block.match(
|
|
/No matching version found for (\S+) published by .+?\. Version (\S+) satisfies the specs but was released at (.+)/
|
|
)
|
|
const chainLines = block
|
|
.split('\n')
|
|
.filter((line: string) => /^\s+at /.test(line))
|
|
.map((line: string) => line.trim().replace(/^at /, ''))
|
|
|
|
if (versionMatch) {
|
|
const [, spec, version, releaseDate] = versionMatch
|
|
console.error(`\n Blocked package: ${spec} (v${version} released ${releaseDate.trim()})`)
|
|
if (chainLines.length > 0) {
|
|
console.error(` Dependency chain: ${chainLines.join(' -> ')}`)
|
|
}
|
|
// Handle scoped (@org/pkg) and unscoped packages: strip the version range suffix
|
|
const pkgName = spec.replace(/@[^/]*$/, '')
|
|
console.error(
|
|
` To unblock, add "${pkgName}" to the minimumReleaseAgeExclude setting in pnpm-workspace.yaml`
|
|
)
|
|
}
|
|
}
|
|
|
|
revert()
|
|
process.exit(1)
|
|
}
|
|
throw error
|
|
}
|
|
|
|
// Remove the override and re-install to see if the lockfile update alone fixes it
|
|
console.log('\nRemoving override and running pnpm install again...')
|
|
fs.writeFileSync(WORKSPACE_YAML_PATH, originalYaml, 'utf-8')
|
|
execSync('pnpm install --silent', {
|
|
stdio: 'pipe',
|
|
encoding: 'utf-8',
|
|
})
|
|
|
|
console.log('\nRunning pnpm audit to verify fix without override...')
|
|
const verifyResult = runAudit()
|
|
|
|
const stillVulnerable = Object.values(verifyResult.advisories).some(
|
|
(adv) => adv.module_name === selected.module_name
|
|
)
|
|
|
|
if (stillVulnerable) {
|
|
revert()
|
|
console.error(
|
|
`\nERROR: Vulnerability for "${selected.module_name}" still present even with override.`
|
|
)
|
|
console.error('Consider using scoped overrides or updating the parent dependency.')
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(
|
|
`\nSUCCESS: Vulnerability for "${selected.module_name}" resolved without needing a permanent override.`
|
|
)
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error('Fatal error:', error)
|
|
process.exit(1)
|
|
})
|