Files
supabase/scripts/fix-audit-vulnerability.ts
T
Ivan Vasilov 83edf14a23 chore: Bump vulnerable dependencies (#44428)
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 -->
2026-04-01 13:10:37 +02:00

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)
})