Files
supabase/scripts/fix-audit-vulnerability.ts
Ivan Vasilov b03866f023 chore: Bump vulnerable dependencies (#43148)
This pull request primarily updates dependencies across the project to
their latest versions, improving compatibility, security, and
performance. It also modifies configuration files to align with the
current package management setup.

Dependency upgrades (core libraries and tools):
Bumps dependencies to solve the following issues:
- https://github.com/supabase/supabase/security/dependabot/2855
- https://github.com/supabase/supabase/security/dependabot/2844
- https://github.com/supabase/supabase/security/dependabot/2860
- https://github.com/supabase/supabase/security/dependabot/2815
- https://github.com/supabase/supabase/security/dependabot/2774
- https://github.com/supabase/supabase/security/dependabot/2836
- https://github.com/supabase/supabase/security/dependabot/2816
- https://github.com/supabase/supabase/security/dependabot/2778
- https://github.com/supabase/supabase/security/dependabot/2790
- https://github.com/supabase/supabase/security/dependabot/2793

Configuration and lock file updates:

* Changed `.prettierignore` to ignore `pnpm-lock.yaml` instead of
`package-lock.json`, reflecting the switch to pnpm as the package
manager.
* Updated dependency overrides in `pnpm-lock.yaml` for `tar` and
`fast-xml-parser` to ensure consistent versions across the workspace.

These updates collectively ensure the project stays current with its
dependencies, reduces potential vulnerabilities, and improves overall
stability and maintainability.
2026-03-02 17:07:55 +01:00

300 lines
8.6 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.`
)
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)
})