diff --git a/packages/blueprints/blueprint/src/resynthesis/merge-strategies/deserialize-strategies.ts b/packages/blueprints/blueprint/src/resynthesis/merge-strategies/deserialize-strategies.ts index 2f72948e3..d6c88ffe0 100644 --- a/packages/blueprints/blueprint/src/resynthesis/merge-strategies/deserialize-strategies.ts +++ b/packages/blueprints/blueprint/src/resynthesis/merge-strategies/deserialize-strategies.ts @@ -10,6 +10,8 @@ import { walkFiles } from '../walk-files'; export type StrategyLocations = { [bundlePath: string]: Strategy[] }; export const LOCAL_STRATEGY_ID = 'local'; +const ALLOWED_LOCAL_CMD = /^[a-zA-Z0-9._/\-]+((\s+(%[AOBLP]|\S+))+)?$/; + const getStrategyIds = (strategies: StrategyLocations): { [identifier: string]: Strategy } => { const ids: { [id: string]: Strategy } = {}; for (const strategyList of Object.values(strategies)) { @@ -47,6 +49,13 @@ export const deserializeStrategies = (existingBundle: string, strategyMatch: Str }); } + if (!ALLOWED_LOCAL_CMD.test(deserializedStrategy.owner)) { + throw new BlueprintSynthesisError({ + message: `Rejected unsafe local merge strategy command: ${deserializedStrategy.owner}`, + type: BlueprintSynthesisErrorTypes.ValidationError, + }); + } + deserializedStrategy.strategy = constructLocalStrategy(deserializedStrategy.owner, ownershipPath); relevantStrategies.push(deserializedStrategy); } else if (deserializedStrategy.identifier in inMemStrategies) { diff --git a/packages/blueprints/blueprint/src/resynthesis/merge-strategies/local.ts b/packages/blueprints/blueprint/src/resynthesis/merge-strategies/local.ts index bc8b0bd78..efaabd148 100644 --- a/packages/blueprints/blueprint/src/resynthesis/merge-strategies/local.ts +++ b/packages/blueprints/blueprint/src/resynthesis/merge-strategies/local.ts @@ -1,4 +1,4 @@ -import { execSync } from 'child_process'; +import { execFileSync } from 'child_process'; import { randomBytes } from 'crypto'; import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; import * as path from 'path'; @@ -47,14 +47,14 @@ function runLocalCommand( ): Buffer | undefined { createTempDir(); - let cmd: string | undefined; + let cmdArgs: string[] | undefined; try { const commonTempFile = createTempFile(buffers.o); const existingTempFile = createTempFile(buffers.a); const proposedTempFile = createTempFile(buffers.b); const cwd = path.dirname(ownershipPath); - cmd = formatLocalCommand( + cmdArgs = formatLocalCommandArgs( owner, path.relative(cwd, existingTempFile), path.relative(cwd, commonTempFile), @@ -62,7 +62,8 @@ function runLocalCommand( resolvedPath, ); - execSync(cmd, { + const [executable, ...args] = cmdArgs; + execFileSync(executable, args, { cwd, }); @@ -70,8 +71,8 @@ function runLocalCommand( return getResolvedFile(existingTempFile); } catch (error: unknown) { let message = 'Failed to run local merge strategy'; - if (cmd) { - message += `: ${cmd}`; + if (cmdArgs) { + message += `: ${cmdArgs.join(' ')}`; } throw new Error(`${message}: ${error}`); @@ -118,12 +119,13 @@ function createTempFile(contents: Buffer): string { return tempPath; } -function formatLocalCommand(commandPattern: string, aPath: string, oPath: string, bPath: string, resolvedPath: string): string { +function formatLocalCommandArgs(commandPattern: string, aPath: string, oPath: string, bPath: string, resolvedPath: string): string[] { // see: https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver - return commandPattern + const substituted = commandPattern .replace(/%O/g, oPath) .replace(/%A/g, aPath) .replace(/%B/g, bPath) .replace(/%P/g, resolvedPath) .replace(/%L/g, `${CONFLICT_MARKER_LENGTH}`); + return substituted.split(/\s+/).filter(s => s.length > 0); } diff --git a/packages/blueprints/sam-serverless-app/src/blueprint.ts b/packages/blueprints/sam-serverless-app/src/blueprint.ts index 27aac3eb6..da563ba35 100644 --- a/packages/blueprints/sam-serverless-app/src/blueprint.ts +++ b/packages/blueprints/sam-serverless-app/src/blueprint.ts @@ -261,7 +261,7 @@ export class Blueprint extends ParentBlueprint { super.synth(); - cp.execSync(`rm -rf ${toDeletePath}`); + fs.rmSync(toDeletePath, { recursive: true, force: true }); // update permissions const permissionChangeContext: FileTemplateContext = { @@ -392,12 +392,10 @@ export class Blueprint extends ParentBlueprint { // `${sourceDir}`, // ]); - const assetPath = path.join('static-assets', 'sam-templates', params.runtime, params.gitSrcPath, '{{cookiecutter.project_name}}', '*'); + const assetDir = path.join('static-assets', 'sam-templates', params.runtime, params.gitSrcPath, '{{cookiecutter.project_name}}'); //TODO: this is a temporary fix to work around SVN failures. These assets need to be updated. - cp.execSync(`cp -R ./${assetPath} ${sourceDir}/`, { - cwd: process.cwd(), - }); + fs.cpSync(path.resolve(process.cwd(), assetDir), sourceDir, { recursive: true }); cp.execFileSync('rm', ['-rf', `${sourceDir}/.svn`, `${sourceDir}/.gitignore`, `${sourceDir}/README.md`, `${sourceDir}/template.yaml`]); diff --git a/packages/utils/blueprint-cli/src/bundle/package-bundle.ts b/packages/utils/blueprint-cli/src/bundle/package-bundle.ts index 6ca685da1..2f07790c1 100644 --- a/packages/utils/blueprint-cli/src/bundle/package-bundle.ts +++ b/packages/utils/blueprint-cli/src/bundle/package-bundle.ts @@ -14,13 +14,13 @@ export function packageBundle( }, ) { try { - let zipCommand = [`zip ${path.basename(outputPathAbs)}`, `-r ${path.basename(folderPathAbs)}`, '-x **/.git/**'].join(' '); - logger.debug(zipCommand); + const zipArgs = [path.basename(outputPathAbs), '-r', path.basename(folderPathAbs), '-x', '**/.git/**']; if (options.encrypt) { logger.info('Packing folder. Enter zip password ...'); - zipCommand = `${zipCommand} -e`; + zipArgs.push('-e'); } - cp.execSync(zipCommand, { + logger.debug(`zip ${zipArgs.join(' ')}`); + cp.execFileSync('zip', zipArgs, { stdio: 'inherit', cwd: path.dirname(folderPathAbs), }); diff --git a/packages/utils/blueprint-cli/src/bundle/write-elements.ts b/packages/utils/blueprint-cli/src/bundle/write-elements.ts index 855511b23..397de1eb7 100644 --- a/packages/utils/blueprint-cli/src/bundle/write-elements.ts +++ b/packages/utils/blueprint-cli/src/bundle/write-elements.ts @@ -27,9 +27,9 @@ export async function writeElements( const sourceLocation = path.join(folderPathAbs, ExportableResource.SRC); fs.mkdirSync(sourceLocation); for (const repositoryName of repositories) { - const cloneCommand = ['git', 'clone', options.bundle.code[repositoryName].clone].join(' '); - logger.debug(`Running: ${cloneCommand}`); - cp.execSync(cloneCommand, { + const cloneArgs = ['clone', options.bundle.code[repositoryName].clone]; + logger.debug(`Running: git ${cloneArgs.join(' ')}`); + cp.execFileSync('git', cloneArgs, { stdio: 'inherit', cwd: sourceLocation, }); diff --git a/packages/utils/blueprint-cli/src/resynth-drivers/resynth.ts b/packages/utils/blueprint-cli/src/resynth-drivers/resynth.ts index ba16a3ccd..025bcfaae 100644 --- a/packages/utils/blueprint-cli/src/resynth-drivers/resynth.ts +++ b/packages/utils/blueprint-cli/src/resynth-drivers/resynth.ts @@ -107,11 +107,7 @@ export async function resynthesize(log: pino.BaseLogger, options: ResynthesizeOp try { // if something already exists at the output location, we remove it. log.debug('cleaning up existing code at resynth resolved output location: %s', resolvedLocation); - const cleanCommand = `rm -rf ${resolvedLocation}`; - cp.execSync(cleanCommand, { - stdio: 'inherit', - cwd: options.blueprint, - }); + fs.rmSync(path.resolve(options.blueprint, resolvedLocation), { recursive: true, force: true }); const timeStart = Date.now(); executeResynthesisCommand(log, options.blueprint, options.jobname, { @@ -220,24 +216,21 @@ const executeResynthesisCommand = ( envVariables?: { [key: string]: string }; }, ) => { - cp.execSync(`mkdir -p ${options.outputDirectory}`, { - stdio: 'inherit', - cwd, - }); + fs.mkdirSync(options.outputDirectory, { recursive: true }); - const command = [ - `npx ${options.driver.runtime}`, - `${options.driver.path}`, - `'${JSON.stringify(options.options)}'`, - `${options.outputDirectory}`, - `${options.entropy}`, - `${options.ancestorBundleDirectory}`, - `${options.existingBundleDirectory}`, - `${options.proposedBundleDirectory}`, - ].join(' '); + const resynthArgs = [ + options.driver.runtime, + options.driver.path, + JSON.stringify(options.options), + options.outputDirectory, + options.entropy, + options.ancestorBundleDirectory, + options.existingBundleDirectory, + options.proposedBundleDirectory, + ]; - logger.debug(`[${jobname}] reynthesis Command: ${command}`); - cp.execSync(command, { + logger.debug(`[${jobname}] reynthesis Command: npx ${resynthArgs.join(' ')}`); + cp.execFileSync('npx', resynthArgs, { stdio: 'inherit', cwd, env: { diff --git a/packages/utils/blueprint-cli/src/synth-drivers/cache.ts b/packages/utils/blueprint-cli/src/synth-drivers/cache.ts index 04ddce59e..57cf39e4c 100644 --- a/packages/utils/blueprint-cli/src/synth-drivers/cache.ts +++ b/packages/utils/blueprint-cli/src/synth-drivers/cache.ts @@ -53,7 +53,7 @@ const fsLinkerPlugin = { name: 'fs_linker', setup(build: any) { build.onStart(() => { - cp.execSync('rm -rf ./lib/externals'); + fs.rmSync('./lib/externals', { recursive: true, force: true }); }); build.onLoad({ filter: /\.(t|j)s/ }, ({ path: filePath }) => { @@ -73,9 +73,16 @@ const fsLinkerPlugin = { const filesSegments = filePath.split('/'); const rootPkg = findParentPackageDir(filesSegments.slice(0, filesSegments.length - 1).join('/')); const pkgName = JSON.parse(fs.readFileSync(`${rootPkg}/package.json`, { encoding: 'utf-8' })).name; - cp.execSync( - `mkdir -p ./lib/externals/${pkgName} && rsync -a ${rootPkg} ./lib/externals/${pkgName} --include="*/" --exclude="node_modules/**" --prune-empty-dirs`, - ); + const externalsDir = `./lib/externals/${pkgName}`; + fs.mkdirSync(externalsDir, { recursive: true }); + cp.execFileSync('rsync', [ + '-a', + rootPkg, + externalsDir, + '--include=*/', + '--exclude=node_modules/**', + '--prune-empty-dirs', + ]); } return { contents, @@ -116,11 +123,9 @@ export const createCache = async ( resynthCacheFile, ]; cleanup.forEach(file => { - if (fs.existsSync(path.join(params.buildDirectory, file))) { - cp.execSync(`rm -rf ${file}`, { - stdio: 'inherit', - cwd: params.buildDirectory, - }); + const filePath = path.join(params.buildDirectory, file); + if (fs.existsSync(filePath)) { + fs.rmSync(filePath, { recursive: true, force: true }); } }); @@ -147,16 +152,10 @@ export const createCache = async ( })); // clean up the synth driver - cp.execSync(`rm ${synthDriver}`, { - stdio: 'inherit', - cwd: params.buildDirectory, - }); + fs.rmSync(path.join(params.buildDirectory, synthDriver), { force: true }); // clean up the resynth driver - cp.execSync(`rm ${resynthDriver}`, { - stdio: 'inherit', - cwd: params.buildDirectory, - }); + fs.rmSync(path.join(params.buildDirectory, resynthDriver), { force: true }); return { synthDriver: path.join(params.buildDirectory, synthCacheFile), diff --git a/packages/utils/blueprint-cli/src/synth-drivers/synth-driver.ts b/packages/utils/blueprint-cli/src/synth-drivers/synth-driver.ts index 52fb5cf2f..71194eae1 100644 --- a/packages/utils/blueprint-cli/src/synth-drivers/synth-driver.ts +++ b/packages/utils/blueprint-cli/src/synth-drivers/synth-driver.ts @@ -1,4 +1,3 @@ -import * as cp from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as deepmerge from 'deepmerge'; @@ -132,9 +131,7 @@ export const makeDriverFile = async ( export const cleanUpDriver = (log: pino.BaseLogger, file: DriverFile) => { log.debug(`Cleaning up driver: ${file.path}`); - cp.execSync(`rm ${file.path}`, { - stdio: 'inherit', - }); + fs.rmSync(file.path, { force: true }); }; /** diff --git a/packages/utils/blueprint-cli/src/synth-drivers/synth.ts b/packages/utils/blueprint-cli/src/synth-drivers/synth.ts index 2f3ef0923..05c932c79 100644 --- a/packages/utils/blueprint-cli/src/synth-drivers/synth.ts +++ b/packages/utils/blueprint-cli/src/synth-drivers/synth.ts @@ -67,11 +67,7 @@ export async function synthesize(log: pino.BaseLogger, options: SynthOptions) { try { // if something already exists at the synthesis location, we remove it. log.debug('cleaning up existing code at synth location: %s', options.outputDirectory); - const cleanCommand = `rm -rf ${options.outputDirectory}`; - cp.execSync(cleanCommand, { - stdio: 'inherit', - cwd: options.blueprintPath, - }); + fs.rmSync(path.resolve(options.blueprintPath, options.outputDirectory), { recursive: true, force: true }); const timeStart = Date.now(); executeSynthesisCommand(log, options.blueprintPath, options.jobname, { @@ -127,20 +123,18 @@ function executeSynthesisCommand( envVariables: { [key: string]: string }; }, ) { - cp.execSync(`mkdir -p ${options.outputDirectory}`, { - stdio: 'inherit', - cwd, - }); - const synthCommand = [ - `npx ${options.driver.runtime}`, - `${options.driver.path}`, - `'${JSON.stringify(options.options)}'`, - `'${options.outputDirectory}'`, - `'${options.entropy}'`, - ].join(' '); - - logger.debug(`[${jobname}] Synthesis Command: ${synthCommand}`); - cp.execSync(synthCommand, { + fs.mkdirSync(options.outputDirectory, { recursive: true }); + + const synthArgs = [ + options.driver.runtime, + options.driver.path, + JSON.stringify(options.options), + options.outputDirectory, + options.entropy, + ]; + + logger.debug(`[${jobname}] Synthesis Command: npx ${synthArgs.join(' ')}`); + cp.execFileSync('npx', synthArgs, { stdio: 'inherit', cwd, env: {