mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
This PR bumps various dependencies to fix vulnerabilities. The logic for bumping packages has been taken out of `fix-audit-vulnerability` into a `bump-package` script. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Chores** * Removed unused development dependency from generator package * Updated package version overrides and vulnerability management configuration to address security concerns * Enhanced internal package dependency maintenance tooling for improved operational efficiency <!-- end of auto-generated comment: release notes by coderabbit.ai -->
210 lines
5.6 KiB
TypeScript
210 lines
5.6 KiB
TypeScript
import { execSync } from 'node:child_process'
|
|
import * as fs from 'node:fs'
|
|
import * as readline from 'node:readline'
|
|
import { bumpPackage, compareSemver, LOCKFILE_PATH } from './bump-package'
|
|
|
|
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 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 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])
|
|
})
|
|
})
|
|
}
|
|
|
|
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 lockfile to revert if the audit verify step fails
|
|
const originalLockfile = fs.readFileSync(LOCKFILE_PATH, 'utf-8')
|
|
|
|
try {
|
|
await bumpPackage(selected.module_name, selected.overrideVersion)
|
|
} catch (error: any) {
|
|
console.error(error.message ?? error)
|
|
process.exit(1)
|
|
}
|
|
|
|
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) {
|
|
console.log('\nReverting pnpm-lock.yaml...')
|
|
fs.writeFileSync(LOCKFILE_PATH, originalLockfile, 'utf-8')
|
|
console.log('Reverted to original state.')
|
|
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)
|
|
})
|