diff --git a/node-src/lib/findChangedDependencies.test.ts b/node-src/lib/findChangedDependencies.test.ts index 7fc77c1d4..d712deff3 100644 --- a/node-src/lib/findChangedDependencies.test.ts +++ b/node-src/lib/findChangedDependencies.test.ts @@ -1,5 +1,6 @@ +import { statSync as unMockedStatSync } from 'fs'; import { buildDepTreeFromFiles } from 'snyk-nodejs-lockfile-parser'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { Context } from '..'; import * as git from '../git/git'; @@ -9,6 +10,10 @@ import TestLogger from './testLogger'; vi.mock('snyk-nodejs-lockfile-parser'); vi.mock('yarn-or-npm'); vi.mock('../git/git'); +vi.mock('fs'); + +const statSync = unMockedStatSync as Mock; +statSync.mockReturnValue({ size: 1 }); const getRepositoryRoot = vi.mocked(git.getRepositoryRoot); const checkoutFile = vi.mocked(git.checkoutFile); diff --git a/node-src/lib/getDependencies.test.ts b/node-src/lib/getDependencies.test.ts index 30ab767eb..68190405d 100644 --- a/node-src/lib/getDependencies.test.ts +++ b/node-src/lib/getDependencies.test.ts @@ -1,16 +1,22 @@ +import { statSync as unMockedStatSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, Mock, vi } from 'vitest'; import packageJson from '../__mocks__/dependencyChanges/plain-package.json'; import { checkoutFile } from '../git/git'; -import { getDependencies } from './getDependencies'; +import { getDependencies, MAX_LOCK_FILE_SIZE } from './getDependencies'; import TestLogger from './testLogger'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ctx = { log: new TestLogger() } as any; +vi.mock('fs'); + +const statSync = unMockedStatSync as Mock; +statSync.mockReturnValue({ size: 1 }); + describe('getDependencies', () => { it('should return a set of dependencies', async () => { const dependencies = await getDependencies(ctx, { @@ -99,4 +105,38 @@ describe('getDependencies', () => { ]) ); }); + + it('should bail if the lock file is too large to parse', async () => { + statSync.mockReturnValue({ size: MAX_LOCK_FILE_SIZE + 1000 }); + + await expect(() => + getDependencies(ctx, { + rootPath: path.join(__dirname, '../__mocks__/dependencyChanges'), + manifestPath: 'plain-package.json', + lockfilePath: 'plain-yarn.lock', + }) + ).rejects.toThrowError(); + }); + + it('should use MAX_LOCK_FILE_SIZE environment variable, if set', async () => { + vi.stubEnv('MAX_LOCK_FILE_SIZE', (MAX_LOCK_FILE_SIZE + 2000).toString()); + statSync.mockReturnValue({ size: MAX_LOCK_FILE_SIZE + 1000 }); + + const dependencies = await getDependencies(ctx, { + rootPath: path.join(__dirname, '../__mocks__/dependencyChanges'), + manifestPath: 'plain-package.json', + lockfilePath: 'plain-yarn.lock', + }); + + const [dep] = dependencies; + expect(dep).toMatch(/^[\w/@-]+@@[\d.]+$/); + + const dependencyNames = [...dependencies].map((dependency) => dependency.split('@@')[0]); + expect(dependencyNames).toEqual( + expect.arrayContaining([ + ...Object.keys(packageJson.dependencies), + ...Object.keys(packageJson.devDependencies), + ]) + ); + }); }); diff --git a/node-src/lib/getDependencies.ts b/node-src/lib/getDependencies.ts index 63340dec9..23c7b5695 100644 --- a/node-src/lib/getDependencies.ts +++ b/node-src/lib/getDependencies.ts @@ -1,7 +1,11 @@ +import { statSync } from 'fs'; +import path from 'path'; import { buildDepTreeFromFiles, PkgTree } from 'snyk-nodejs-lockfile-parser'; import { Context } from '../types'; +export const MAX_LOCK_FILE_SIZE = 10_485_760; // 10 MB + export const getDependencies = async ( ctx: Context, { @@ -19,6 +23,10 @@ export const getDependencies = async ( strictOutOfSync?: boolean; } ) => { + // We can run into OOM errors if the lock file is too large. Therefore, we bail early and skip + // lock file parsing because some TurboSnap is better than no TurboSnap. + ensureLockFileSize(ctx, path.resolve(rootPath, lockfilePath)); + try { const headTree = await buildDepTreeFromFiles( rootPath, @@ -42,3 +50,14 @@ function flattenDependencyTree(tree: PkgTree['dependencies'], results = new Set< return results; } + +function ensureLockFileSize(ctx: Context, fullPath: string) { + const maxLockFileSize = + Number.parseInt(process.env.MAX_LOCK_FILE_SIZE ?? '') || MAX_LOCK_FILE_SIZE; + + const stats = statSync(fullPath); + if (stats.size > maxLockFileSize) { + ctx.log.warn({ fullPath }, 'Lock file too large to parse, skipping'); + throw new Error('Lock file too large to parse'); + } +}