diff --git a/scripts/dev.js b/scripts/dev.js index a4a44114971..5388b9af082 100644 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -38,7 +38,7 @@ console.log(`\n${colors.bright}${colors.yellow}๐Ÿš€ Starting Bruno development e const webProcess = spawn('npm', ['run', 'dev'], { cwd: webDir, stdio: ['inherit', 'pipe', 'pipe'], - shell: true + shell: process.platform === 'win32' }); webProcess.stdout.on('data', (data) => { @@ -71,7 +71,7 @@ function startElectron(port) { electronProcess = spawn('npm', ['run', 'dev'], { cwd: electronDir, stdio: 'inherit', - shell: true, + shell: process.platform === 'win32', env: { ...process.env, BRUNO_DEV_PORT: port diff --git a/scripts/patch-security.js b/scripts/patch-security.js new file mode 100644 index 00000000000..1a3ff79c6ce --- /dev/null +++ b/scripts/patch-security.js @@ -0,0 +1,162 @@ +const fs = require('fs'); +const path = require('path'); + +// โ”€โ”€ Patch tables โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Keep these current. Each entry is idempotent: re-running is safe. + +const REPLACE_DEV_DEPS = { + // deprecated rollup-plugin-terser ships serialize-javascript <=7.0.4 (RCE) + 'rollup-plugin-terser': { pkg: '@rollup/plugin-terser', version: '^1.0.0' } +}; + +const PIN_DEPS = { + '@aws-sdk/credential-providers': '3.1019.0', + '@rsbuild/plugin-styled-components': '^1.1.0' +}; + +// Patches for root-level npm overrides (simple string values only). +const OVERRIDE_PATCHES = { + rollup: '3.30.0' +}; + +// Exact versions to force-install after `npm install`, bypassing broken +// npm-overrides-in-workspaces. Each key is a package name; each value is the +// exact safe version to install. setup.js runs: +// npm install --no-save @ ... +const FORCE_INSTALL_VERSIONS = { + '@aws-sdk/client-sts': '3.1019.0', + 'fast-xml-parser': '5.5.9', + flatted: '3.4.2', + 'form-data': '4.0.5', + glob: '10.5.0', + immutable: '5.1.5', + minimatch: '3.1.5', + 'path-to-regexp': '0.1.13', + pbkdf2: '3.1.5', + picomatch: '2.3.2', + svgo: '2.8.2', + tar: '7.5.13', + undici: '6.24.1' +}; + +const ROLLUP_TERSER_RE = /const\s*\{\s*terser\s*\}\s*=\s*require\(['"]rollup-plugin-terser['"]\)/g; +const ROLLUP_TERSER_NEW = "const terser = require('@rollup/plugin-terser')"; + +// โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function writeJson(filePath, obj) { + fs.writeFileSync(filePath, JSON.stringify(obj, null, 2) + '\n'); +} + +function findWorkspacePackageJsons(rootDir) { + const root = readJson(path.join(rootDir, 'package.json')); + const results = []; + for (const ws of root.workspaces || []) { + const pkgPath = path.join(rootDir, ws, 'package.json'); + if (fs.existsSync(pkgPath)) { + results.push(pkgPath); + } + } + return results; +} + +// โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +function patchSecurityVulnerabilities(rootDir) { + rootDir = rootDir || process.cwd(); + + // Add force-install versions as root devDependencies so npm hoists + // the safe version and updates the lockfile + const rootPkgPath = path.join(rootDir, 'package.json'); + const rootPkg = readJson(rootPkgPath); + let rootChanged = false; + rootPkg.devDependencies = rootPkg.devDependencies || {}; + for (const [dep, ver] of Object.entries(FORCE_INSTALL_VERSIONS)) { + if (rootPkg.devDependencies[dep] !== ver) { + rootPkg.devDependencies[dep] = ver; + rootChanged = true; + } + } + // Patch root-level overrides + if (rootPkg.overrides) { + for (const [dep, ver] of Object.entries(OVERRIDE_PATCHES)) { + if (typeof rootPkg.overrides[dep] === 'string' && rootPkg.overrides[dep] !== ver) { + rootPkg.overrides[dep] = ver; + rootChanged = true; + } + } + } + + if (rootChanged) { + writeJson(rootPkgPath, rootPkg); + // Delete stale lockfile so npm resolves fresh versions with the new devDeps + const lockPath = path.join(rootDir, 'package-lock.json'); + if (fs.existsSync(lockPath)) { + fs.unlinkSync(lockPath); + } + } + + // Workspace packages + const workspacePkgs = findWorkspacePackageJsons(rootDir); + for (const pkgPath of workspacePkgs) { + const pkg = readJson(pkgPath); + let changed = false; + + for (const section of ['dependencies', 'devDependencies']) { + const deps = pkg[section]; + if (!deps) continue; + + // Replace deprecated packages + for (const [oldName, { pkg: newPkg, version }] of Object.entries(REPLACE_DEV_DEPS)) { + if (deps[oldName] !== undefined) { + delete deps[oldName]; + deps[newPkg] = version; + changed = true; + } + } + + // Update exact pins + for (const [dep, newVer] of Object.entries(PIN_DEPS)) { + if (deps[dep] !== undefined && deps[dep] !== newVer) { + deps[dep] = newVer; + changed = true; + } + } + } + + if (changed) { + writeJson(pkgPath, pkg); + } + + // 3. Patch any .js file in the workspace package that requires rollup-plugin-terser + patchJsFilesRecursive(path.dirname(pkgPath)); + } +} + +function patchJsFilesRecursive(dir) { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === 'dist') continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + patchJsFilesRecursive(full); + } else if (entry.name.endsWith('.js')) { + const src = fs.readFileSync(full, 'utf8'); + ROLLUP_TERSER_RE.lastIndex = 0; + if (ROLLUP_TERSER_RE.test(src)) { + ROLLUP_TERSER_RE.lastIndex = 0; + fs.writeFileSync(full, src.replace(ROLLUP_TERSER_RE, ROLLUP_TERSER_NEW)); + } + } + } +} + +function buildForceInstallArgs() { + return Object.entries(FORCE_INSTALL_VERSIONS).map(([pkg, ver]) => `${pkg}@${ver}`); +} + +module.exports = { patchSecurityVulnerabilities, buildForceInstallArgs, REPLACE_DEV_DEPS, PIN_DEPS, OVERRIDE_PATCHES, FORCE_INSTALL_VERSIONS }; \ No newline at end of file diff --git a/scripts/patch-security.test.js b/scripts/patch-security.test.js new file mode 100644 index 00000000000..3e1df3f7677 --- /dev/null +++ b/scripts/patch-security.test.js @@ -0,0 +1,337 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const { patchSecurityVulnerabilities, buildForceInstallArgs } = require('./patch-security'); + +function makeTmpDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-patch-test-')); +} + +function writeJson(dir, relPath, obj) { + const full = path.join(dir, relPath); + fs.mkdirSync(path.dirname(full), { recursive: true }); + fs.writeFileSync(full, JSON.stringify(obj, null, 2) + '\n'); +} + +function readJson(dir, relPath) { + return JSON.parse(fs.readFileSync(path.join(dir, relPath), 'utf8')); +} + +function writeFile(dir, relPath, content) { + const full = path.join(dir, relPath); + fs.mkdirSync(path.dirname(full), { recursive: true }); + fs.writeFileSync(full, content); +} + +function readFile(dir, relPath) { + return fs.readFileSync(path.join(dir, relPath), 'utf8'); +} + +describe('patchSecurityVulnerabilities', () => { + test('does not add transitive-dep overrides to root package.json', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: { rollup: '3.30.0' } + }); + writeJson(tmp, 'packages/foo/package.json', { name: 'foo' }); + + patchSecurityVulnerabilities(tmp); + + const root = readJson(tmp, 'package.json'); + // preserves existing overrides + expect(root.overrides.rollup).toBe('3.30.0'); + // does NOT add transitive-dep overrides (they conflict with force-install) + expect(root.overrides.tar).toBeUndefined(); + expect(root.overrides.pbkdf2).toBeUndefined(); + }); + + test('replaces rollup-plugin-terser with @rollup/plugin-terser in devDependencies', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: {} + }); + writeJson(tmp, 'packages/foo/package.json', { + name: 'foo', + devDependencies: { + '@rollup/plugin-commonjs': '^23.0.2', + 'rollup-plugin-terser': '^7.0.2' + } + }); + + patchSecurityVulnerabilities(tmp); + + const pkg = readJson(tmp, 'packages/foo/package.json'); + expect(pkg.devDependencies['rollup-plugin-terser']).toBeUndefined(); + expect(pkg.devDependencies['@rollup/plugin-terser']).toBe('^1.0.0'); + expect(pkg.devDependencies['@rollup/plugin-commonjs']).toBe('^23.0.2'); + }); + + test('patches rollup.config.js require statement', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: {} + }); + writeJson(tmp, 'packages/foo/package.json', { + name: 'foo', + devDependencies: { 'rollup-plugin-terser': '^7.0.2' } + }); + writeFile( + tmp, + 'packages/foo/rollup.config.js', + "const { terser } = require('rollup-plugin-terser');\nmodule.exports = { plugins: [terser()] };\n" + ); + + patchSecurityVulnerabilities(tmp); + + const config = readFile(tmp, 'packages/foo/rollup.config.js'); + expect(config).toContain("const terser = require('@rollup/plugin-terser')"); + expect(config).not.toContain('rollup-plugin-terser'); + }); + + test('updates pinned @aws-sdk/credential-providers version', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: {} + }); + writeJson(tmp, 'packages/foo/package.json', { + name: 'foo', + dependencies: { '@aws-sdk/credential-providers': '3.1017.0' } + }); + + patchSecurityVulnerabilities(tmp); + + const pkg = readJson(tmp, 'packages/foo/package.json'); + expect(pkg.dependencies['@aws-sdk/credential-providers']).toBe('3.1019.0'); + }); + + test('is idempotent โ€” running twice produces the same result', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: { rollup: '3.30.0' } + }); + writeJson(tmp, 'packages/foo/package.json', { + name: 'foo', + devDependencies: { 'rollup-plugin-terser': '^7.0.2' }, + dependencies: { '@aws-sdk/credential-providers': '3.1017.0' } + }); + writeFile( + tmp, + 'packages/foo/rollup.config.js', + "const { terser } = require('rollup-plugin-terser');\n" + ); + + patchSecurityVulnerabilities(tmp); + const afterFirst = readJson(tmp, 'packages/foo/package.json'); + const rootAfterFirst = readJson(tmp, 'package.json'); + const configAfterFirst = readFile(tmp, 'packages/foo/rollup.config.js'); + + patchSecurityVulnerabilities(tmp); + const afterSecond = readJson(tmp, 'packages/foo/package.json'); + const rootAfterSecond = readJson(tmp, 'package.json'); + const configAfterSecond = readFile(tmp, 'packages/foo/rollup.config.js'); + + expect(afterSecond).toEqual(afterFirst); + expect(rootAfterSecond).toEqual(rootAfterFirst); + expect(configAfterSecond).toBe(configAfterFirst); + }); + + test('handles double-quoted require in rollup config', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: {} + }); + writeJson(tmp, 'packages/foo/package.json', { + name: 'foo', + devDependencies: { 'rollup-plugin-terser': '^7.0.2' } + }); + writeFile( + tmp, + 'packages/foo/rollup.config.js', + 'const { terser } = require("rollup-plugin-terser");\n' + ); + + patchSecurityVulnerabilities(tmp); + + const config = readFile(tmp, 'packages/foo/rollup.config.js'); + expect(config).toContain("const terser = require('@rollup/plugin-terser')"); + expect(config).not.toContain('rollup-plugin-terser'); + }); + + test('patches rollup-plugin-terser require in non-config JS files within workspace', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: {} + }); + writeJson(tmp, 'packages/foo/package.json', { + name: 'foo', + devDependencies: { 'rollup-plugin-terser': '^7.0.2' } + }); + writeFile( + tmp, + 'packages/foo/src/sandbox/bundle-libraries.js', + "const rollup = require('rollup');\nconst { terser } = require('rollup-plugin-terser');\nmodule.exports = {};\n" + ); + + patchSecurityVulnerabilities(tmp); + + const bundler = readFile(tmp, 'packages/foo/src/sandbox/bundle-libraries.js'); + expect(bundler).toContain("const terser = require('@rollup/plugin-terser')"); + expect(bundler).not.toContain('rollup-plugin-terser'); + }); + + test('skips packages without vulnerable deps', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/clean'], + overrides: {} + }); + const original = { + name: 'clean', + dependencies: { lodash: '^4.17.21' } + }; + writeJson(tmp, 'packages/clean/package.json', original); + + patchSecurityVulnerabilities(tmp); + + const pkg = readJson(tmp, 'packages/clean/package.json'); + expect(pkg).toEqual(original); + }); +}); + +describe('patchSecurityVulnerabilities applies override patches to root overrides', () => { + test('updates vulnerable override versions in root package.json', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: { rollup: '3.29.5' } + }); + writeJson(tmp, 'packages/foo/package.json', { name: 'foo' }); + + patchSecurityVulnerabilities(tmp); + + const root = readJson(tmp, 'package.json'); + expect(root.overrides.rollup).toBe('3.30.0'); + }); + + test('preserves unrelated overrides while patching', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: { + rollup: '3.29.5', + 'electron-store': { conf: { 'json-schema-typed': '8.0.1' } } + } + }); + writeJson(tmp, 'packages/foo/package.json', { name: 'foo' }); + + patchSecurityVulnerabilities(tmp); + + const root = readJson(tmp, 'package.json'); + expect(root.overrides.rollup).toBe('3.30.0'); + expect(root.overrides['electron-store']).toEqual({ conf: { 'json-schema-typed': '8.0.1' } }); + }); + + test('deletes lockfile when overrides change', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + overrides: { rollup: '3.29.5' } + }); + writeJson(tmp, 'packages/foo/package.json', { name: 'foo' }); + fs.writeFileSync(path.join(tmp, 'package-lock.json'), '{"stale": true}'); + + patchSecurityVulnerabilities(tmp); + + expect(fs.existsSync(path.join(tmp, 'package-lock.json'))).toBe(false); + }); +}); + +describe('patchSecurityVulnerabilities pins workspace devDependencies', () => { + test('updates exact-pinned @rsbuild/plugin-styled-components to caret range', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/app'] + }); + writeJson(tmp, 'packages/app/package.json', { + name: 'app', + devDependencies: { '@rsbuild/plugin-styled-components': '1.1.0' } + }); + + patchSecurityVulnerabilities(tmp); + + const pkg = readJson(tmp, 'packages/app/package.json'); + expect(pkg.devDependencies['@rsbuild/plugin-styled-components']).toBe('^1.1.0'); + }); +}); + +describe('buildForceInstallArgs', () => { + test('returns array of pkg@version strings for all force-install targets', () => { + const args = buildForceInstallArgs(); + expect(Array.isArray(args)).toBe(true); + expect(args.length).toBeGreaterThan(0); + for (const arg of args) { + expect(arg).toMatch(/^[@a-z].*@\d/); + } + expect(args).toContain('pbkdf2@3.1.5'); + expect(args).toContain('undici@6.24.1'); + expect(args).toContain('picomatch@2.3.2'); + expect(args).toContain('glob@10.5.0'); + }); +}); + +describe('patchSecurityVulnerabilities adds force-install versions to root devDependencies', () => { + test('adds FORCE_INSTALL_VERSIONS as root devDependencies for hoisting', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + devDependencies: { jest: '^29.2.0' } + }); + writeJson(tmp, 'packages/foo/package.json', { name: 'foo' }); + + patchSecurityVulnerabilities(tmp); + + const root = readJson(tmp, 'package.json'); + expect(root.devDependencies.pbkdf2).toBe('3.1.5'); + expect(root.devDependencies.undici).toBe('6.24.1'); + expect(root.devDependencies['fast-xml-parser']).toBe('5.5.9'); + // preserves existing devDeps + expect(root.devDependencies.jest).toBe('^29.2.0'); + }); + + test('deletes package-lock.json so npm resolves fresh versions', () => { + const tmp = makeTmpDir(); + writeJson(tmp, 'package.json', { + name: 'test-root', + workspaces: ['packages/foo'], + devDependencies: {} + }); + writeJson(tmp, 'packages/foo/package.json', { name: 'foo' }); + // Create a stale lockfile + fs.writeFileSync(path.join(tmp, 'package-lock.json'), '{"stale": true}'); + + patchSecurityVulnerabilities(tmp); + + expect(fs.existsSync(path.join(tmp, 'package-lock.json'))).toBe(false); + }); +}); \ No newline at end of file diff --git a/scripts/setup.js b/scripts/setup.js index cf1e9a70e19..abc0277b13f 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -1,6 +1,7 @@ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); +const { patchSecurityVulnerabilities } = require('./patch-security'); const icons = { clean: '๐Ÿงน', @@ -71,7 +72,7 @@ function forceInstallPlatformDeps() { const toInstall = deps[process.platform]; execCommand( - `npm i --legacy-peer-deps --no-save --force ${toInstall.join(' ')}`, + `npm install --no-save --force ${toInstall.join(' ')}`, 'Installing platform specific dependencies' ); } @@ -86,10 +87,24 @@ async function setup() { fs.rmSync(dir, { recursive: true, force: true }); } + // Patch known security vulnerabilities in package.json files before install + console.log(`\n๐Ÿ”’ Patching security vulnerabilities...`); + patchSecurityVulnerabilities(); + console.log(`${icons.success} Security patches applied`); + // Install dependencies - execCommand('npm i --legacy-peer-deps', 'Installing dependencies'); + execCommand('npm install --legacy-peer-deps', 'Installing dependencies'); forceInstallPlatformDeps(); + // Force-install patched versions of vulnerable transitive deps. + // --legacy-peer-deps disables npm overrides, so we install them explicitly. + const { buildForceInstallArgs } = require('./patch-security'); + const secPkgs = buildForceInstallArgs(); + execCommand( + `npm install --no-save --legacy-peer-deps ${secPkgs.join(' ')}`, + 'Patching vulnerable transitive dependencies' + ); + // Build packages execCommand('npm run build:graphql-docs', 'Building graphql-docs'); execCommand('npm run build:bruno-query', 'Building bruno-query'); @@ -110,6 +125,7 @@ async function setup() { } } + setup().catch((error) => { console.error(error); process.exit(1);