UNPKG

lavamoat

Version:

`lavamoat` is a NodeJS runtime where modules are defined in [SES][SesGithub] Compartments. It aims to reduce the risk of malicious code in the app dependency graph, known as "software supply chain attacks".

236 lines (221 loc) 7.79 kB
/* eslint no-eval: 0 */ const fs = require('node:fs') const path = require('node:path') const resolve = require('resolve') const { sanitize } = require('htmlescape') const { generateKernel, applySourceTransforms, makeInitStatsHook, } = require('lavamoat-core') const { getPackageNameForModulePath } = require('@lavamoat/aa') const { checkForResolutionOverride } = require('./resolutions') const { createFreshRealmCompartment } = require('./freshRealmCompartment') const { makeResolver } = require('./resolve') const noop = () => {} const nativeRequire = require module.exports = { createKernel } function createKernel({ projectRoot, lavamoatPolicy, canonicalNameMap, debugMode, statsMode, scuttleGlobalThis, scuttleGlobalThisExceptions, }) { if (scuttleGlobalThisExceptions) { console.warn( 'Lavamoat - "scuttleGlobalThisExceptions" is deprecated. Use "scuttleGlobalThis.exceptions" instead.' ) if (scuttleGlobalThis === true) { scuttleGlobalThis = { enabled: true } } scuttleGlobalThis = Object.assign({}, scuttleGlobalThis) scuttleGlobalThis.exceptions = scuttleGlobalThis?.exceptions || scuttleGlobalThisExceptions } const { resolutions } = lavamoatPolicy const getRelativeModuleId = createModuleResolver({ projectRoot, resolutions, canonicalNameMap, }) const loadModuleData = createModuleLoader({ canonicalNameMap }) const kernelSrc = generateKernel({ debugMode, scuttleGlobalThis }) const createKernel = evaluateWithSourceUrl('LavaMoat/node/kernel', kernelSrc) const reportStatsHook = statsMode ? makeInitStatsHook({ onStatsReady }) : noop const kernel = createKernel({ lavamoatConfig: lavamoatPolicy, loadModuleData, getRelativeModuleId, prepareModuleInitializerArgs, getExternalCompartment, globalThisRefs: ['global', 'globalThis'], reportStatsHook, }) return kernel } function getExternalCompartment(packageName, packagePolicy) { const envPolicy = packagePolicy.env if (packagePolicy.env === 'unfrozen') { return createFreshRealmCompartment() } throw new Error( `LavaMoat/node - unrecognized "env" policy setting for package "${packageName}", "${envPolicy}"` ) } function prepareModuleInitializerArgs( requireRelativeWithContext, moduleObj, moduleData ) { const require = requireRelativeWithContext const module = moduleObj const exports = moduleObj.exports const __filename = moduleData.file const __dirname = path.dirname(__filename) require.resolve = (requestedName) => { return resolve.sync(requestedName, { basedir: __dirname }) } return [exports, require, module, __filename, __dirname] } function createModuleResolver({ projectRoot, resolutions, canonicalNameMap }) { const resolver = makeResolver({ projectRoot }) return function getRelativeModuleId(parentAbsolutePath, requestedName) { // handle resolution overrides const parentDir = path.dirname(parentAbsolutePath) const parentPackageName = getPackageNameForModulePath( canonicalNameMap, parentDir ) const result = checkForResolutionOverride( resolutions, parentPackageName, requestedName ) if (result) { requestedName = result // if path is a relative path, it should be relative to the projectRoot if (!path.isAbsolute(result)) { const parentFileName = path.basename(parentAbsolutePath) parentAbsolutePath = path.join(projectRoot, parentFileName) } } return resolver(requestedName, parentAbsolutePath) } } function createModuleLoader({ canonicalNameMap }) { const memo = new Map() return function loadModuleData(absolutePath) { if (memo.has(absolutePath)) { return memo.get(absolutePath) } // load builtin modules (eg "fs") if (resolve.isCore(absolutePath)) { return { type: 'builtin', file: absolutePath, package: absolutePath, id: absolutePath, // wrapper around unprotected "require" moduleInitializer: (exports, require, module) => { module.exports = nativeRequire(absolutePath) }, } // load compiled native module } else if (isNativeModule(absolutePath)) { const packageName = getPackageNameForModulePath( canonicalNameMap, absolutePath ) return { type: 'native', file: absolutePath, package: packageName, id: absolutePath, // wrapper around unprotected "require" moduleInitializer: (exports, require, module) => { module.exports = nativeRequire(absolutePath) }, } // load normal user-space module } else { const moduleContent = fs.readFileSync(absolutePath, 'utf8') // apply source transforms let transformedContent = moduleContent // hash bang const contentLines = transformedContent.split('\n') if (contentLines[0].startsWith('#!')) { transformedContent = contentLines.slice(1).join('\n') } transformedContent = applySourceTransforms(transformedContent) const isJSON = /\.json$/.test(absolutePath) // wrap json modules (borrowed from browserify) if (isJSON) { const sanitizedString = sanitize(transformedContent) try { // check json validity JSON.parse(sanitizedString) transformedContent = 'module.exports=' + sanitizedString } catch (err) { err.message = `While parsing ${absolutePath}: ${err.message}` throw err } } // ses needs to take a fucking chill pill if (isJSON) { transformedContent = transformedContent .split('-import-') .join('-imp ort-') } else { transformedContent = transformedContent .split('"babel-plugin-dynamic-import-node') .join('"babel-plugin-dynamic-imp" + "ort-node') .split('"@babel/plugin-proposal-dynamic-import') .join('"@babel/plugin-proposal-dynamic-imp" + "ort') .split('// Re-export lib/utils, so that consumers can import') .join('// Re-export lib/utils, so that consumers can imp_ort') .split('// babel-plugin-dynamic-import') .join('// babel-plugin-dynamic-imp ort') .split('// eslint-disable-next-line import/no-unresolved') .join('// eslint-disable-next-line imp_ort/no-unresolved') } // wrap in moducreateModuleResolverleInitializer // security: ensure module path does not inject code if (absolutePath.includes('\n')) { throw new Error('invalid newline in module source path') } const wrappedContent = `(function(exports, require, module, __filename, __dirname){\n${transformedContent}\n})` const packageName = getPackageNameForModulePath( canonicalNameMap, absolutePath ) const moduleData = { type: 'js', file: absolutePath, package: packageName, source: wrappedContent, id: absolutePath, } memo.set(absolutePath, moduleData) return moduleData } } } function isNativeModule(filename) { const fileExtension = filename.split('.').pop() return fileExtension === 'node' } function evaluateWithSourceUrl(filename, content) { return eval(`${content}\n//# sourceURL=${filename}`) } function onStatsReady(moduleGraphStatsObj) { const graphId = Date.now() console.warn( `completed module graph init "${graphId}" in ${moduleGraphStatsObj.value}ms ("${moduleGraphStatsObj.name}")` ) const statsFilePath = `./lavamoat-flame-${graphId}.json` console.warn(`wrote stats file to "${statsFilePath}"`) fs.writeFileSync(statsFilePath, JSON.stringify(moduleGraphStatsObj, null, 2)) }