mirror of
https://github.com/supabase/supabase.git
synced 2026-05-10 17:11:21 +08:00
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.
300 lines
8.6 KiB
TypeScript
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)
|
|
})
|