Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bin/commands/runs.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ module.exports = function run(args, rawArgs) {
// set the no-wrap
utils.setNoWrap(bsConfig, args);

// process auto-import dev dependencies
utils.processAutoImportDependencies(bsConfig.run_settings);

// add cypress dependency if missing
utils.setCypressNpmDependency(bsConfig);

Expand Down
2 changes: 2 additions & 0 deletions bin/helpers/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ const createBuild = (bsConfig, zip) => {
if(error.response) {
logger.error(utils.formatRequest(error.response.statusText, error.response, error.response.data));
reject(`${Constants.userMessages.BUILD_FAILED} Error: ${error.response.data.message}`);
} else {
reject(error);
}
}
}).catch(function(err){
Expand Down
8 changes: 7 additions & 1 deletion bin/helpers/capabilityHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,17 @@ const getAccessibilityPlatforms = (bsConfig) => {
const addCypressZipStartLocation = (runSettings) => {
let resolvedHomeDirectoryPath = path.resolve(runSettings.home_directory);
let resolvedCypressConfigFilePath = path.resolve(runSettings.cypressConfigFilePath);
runSettings.cypressZipStartLocation = path.dirname(resolvedCypressConfigFilePath.split(resolvedHomeDirectoryPath)[1]);

// Convert to POSIX style paths for consistent behavior
let posixHomePath = resolvedHomeDirectoryPath.split(path.sep).join(path.posix.sep);
let posixConfigPath = resolvedCypressConfigFilePath.split(path.sep).join(path.posix.sep);

runSettings.cypressZipStartLocation = path.posix.dirname(posixConfigPath.split(posixHomePath)[1]);
runSettings.cypressZipStartLocation = runSettings.cypressZipStartLocation.substring(1);
logger.debug(`Setting cypress zip start location = ${runSettings.cypressZipStartLocation}`);
}


const validate = (bsConfig, args) => {
return new Promise(function (resolve, reject) {
logger.info(Constants.userMessages.VALIDATING_CONFIG);
Expand Down
22 changes: 22 additions & 0 deletions bin/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,28 @@ const validationMessages = {
"You have specified '--record' flag but you've not provided the '--record-key' and we could not find any value in 'CYPRESS_RECORD_KEY' environment variable. Your record functionality on cypress.io dashboard might not work as it needs the key and projectId",
NODE_VERSION_PARSING_ERROR:
"We weren't able to successfully parse the specified nodeVersion. We will be using the default nodeVersion to run your tests.",
AUTO_IMPORT_CONFLICT_ERROR:
"Cannot use both 'auto_import_dev_dependencies' and manual npm dependency configuration. Please either set 'auto_import_dev_dependencies' to false or remove manual 'npm_dependencies', 'win_npm_dependencies', and 'mac_npm_dependencies' configurations.",
AUTO_IMPORT_INVALID_TYPE:
"'auto_import_dev_dependencies' must be a boolean value (true or false).",
PACKAGE_JSON_NOT_FOUND:
"package.json not found in project directory. Cannot auto-import devDependencies.",
PACKAGE_JSON_PERMISSION_DENIED:
"Cannot read package.json due to permission issues. Please check file permissions.",
PACKAGE_JSON_MALFORMED:
"package.json contains invalid JSON syntax. Please fix the JSON format.",
PACKAGE_JSON_NOT_OBJECT:
"package.json must contain a JSON object, not an array or other type.",
DEVDEPS_INVALID_FORMAT:
"devDependencies field in package.json must be an object, not an array or other type.",
EXCLUDE_DEPS_INVALID_TYPE:
"'exclude_dependencies' must be an array of strings.",
EXCLUDE_DEPS_INVALID_PATTERNS:
"'exclude_dependencies' must contain only string values representing regex patterns.",
INVALID_REGEX_PATTERN:
"Invalid regex pattern found in 'exclude_dependencies': {pattern}. Please provide valid regex patterns.",
DEPENDENCIES_PARAM_INVALID:
"Dependencies parameter must be an object.",
};

const cliMessages = {
Expand Down
5 changes: 3 additions & 2 deletions bin/helpers/packageInstaller.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ const setupPackageFolder = (runSettings, directoryPath) => {
}

// Combine win and mac specific dependencies if present
if (typeof runSettings.npm_dependencies === 'object') {
const combinedDependencies = combineMacWinNpmDependencies(runSettings);
if (combinedDependencies && Object.keys(combinedDependencies).length > 0) {
Object.assign(packageJSON, {
devDependencies: combineMacWinNpmDependencies(runSettings),
devDependencies: combinedDependencies,
});
}

Expand Down
183 changes: 172 additions & 11 deletions bin/helpers/readCypressConfigUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,159 @@
return constants.CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS.includes(extension) ? extension : 'js'
}

function resolveTsConfigPath(bsConfig, cypress_config_filepath) {
const utils = require("./utils");
const working_dir = path.dirname(cypress_config_filepath);
const project_root = process.cwd();

// Priority order for finding tsconfig with secure path validation
const candidates = [];

// User specified path - validate it's within project bounds
if (bsConfig.run_settings && bsConfig.run_settings.ts_config_file_path) {
try {
const safePath = utils.validateSecurePath(bsConfig.run_settings.ts_config_file_path, project_root);
candidates.push(safePath);
} catch (error) {
logger.warn(`Invalid user-specified tsconfig path: ${error.message}`);
}
}

// Safe predefined paths
try {
candidates.push(utils.validateSecurePath(path.join(working_dir, 'tsconfig.json'), project_root));

Check warning

Code scanning / Semgrep OSS

Semgrep Finding: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal Warning

Detected possible user input going into a path.join or path.resolve function. This could possibly lead to a path traversal vulnerability, where the attacker can access arbitrary files stored in the file system. Instead, be sure to sanitize or validate user input first.
} catch (error) {
logger.debug(`Working directory tsconfig path validation failed: ${error.message}`);
}

try {
candidates.push(utils.validateSecurePath(path.join(working_dir, '..', 'tsconfig.json'), project_root));

Check warning

Code scanning / Semgrep OSS

Semgrep Finding: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal Warning

Detected possible user input going into a path.join or path.resolve function. This could possibly lead to a path traversal vulnerability, where the attacker can access arbitrary files stored in the file system. Instead, be sure to sanitize or validate user input first.
} catch (error) {
logger.debug(`Parent directory tsconfig path validation failed: ${error.message}`);
}

candidates.push(path.join(project_root, 'tsconfig.json'));

for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
logger.debug(`Found tsconfig at: ${candidate}`);
return candidate;
}
}

return null;
}

function generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, complied_js_dir, cypress_config_filepath) {
const utils = require("./utils");
const working_dir = path.dirname(cypress_config_filepath);
const project_root = process.cwd();

// Sanitize and validate paths to prevent command injection
let safe_bstack_node_modules_path;
let safe_cypress_config_filepath;
let safe_working_dir;

try {
safe_bstack_node_modules_path = utils.sanitizeCommandPath(bstack_node_modules_path);
safe_cypress_config_filepath = utils.sanitizeCommandPath(cypress_config_filepath);
safe_working_dir = utils.sanitizeCommandPath(working_dir);

// Additional validation - ensure paths are within expected bounds
utils.validateSecurePath(safe_bstack_node_modules_path, project_root);
utils.validateSecurePath(safe_cypress_config_filepath, project_root);
} catch (error) {
throw new Error(`Invalid file paths detected: ${error.message}`);
}

const typescript_path = path.join(safe_bstack_node_modules_path, 'typescript', 'bin', 'tsc');

Check warning

Code scanning / Semgrep OSS

Semgrep Finding: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal Warning

Detected possible user input going into a path.join or path.resolve function. This could possibly lead to a path traversal vulnerability, where the attacker can access arbitrary files stored in the file system. Instead, be sure to sanitize or validate user input first.
const tsc_alias_path = path.join(safe_bstack_node_modules_path, 'tsc-alias', 'dist', 'bin', 'index.js');

Check warning

Code scanning / Semgrep OSS

Semgrep Finding: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal Warning

Detected possible user input going into a path.join or path.resolve function. This could possibly lead to a path traversal vulnerability, where the attacker can access arbitrary files stored in the file system. Instead, be sure to sanitize or validate user input first.

// Smart tsconfig detection and validation
const resolvedTsConfigPath = resolveTsConfigPath(bsConfig, safe_cypress_config_filepath);
let hasValidTsConfig = false;

if (resolvedTsConfigPath) {
try {
// Validate the tsconfig is readable and valid JSON
const tsConfigContent = fs.readFileSync(resolvedTsConfigPath, 'utf8');
JSON.parse(tsConfigContent);
hasValidTsConfig = true;
logger.info(`Using existing tsconfig: ${resolvedTsConfigPath}`);
} catch (error) {
logger.warn(`Invalid tsconfig file: ${resolvedTsConfigPath}, falling back to default configuration. Error: ${error.message}`);
hasValidTsConfig = false;
}
} else {
logger.info('No tsconfig found, using default TypeScript configuration');
}

let tempTsConfig;

if (hasValidTsConfig) {
// Scenario 1: User has valid tsconfig - use extends approach
tempTsConfig = {
extends: resolvedTsConfigPath,
compilerOptions: {
// Force override critical parameters for BrowserStack compatibility
"outDir": path.basename(complied_js_dir),
"listEmittedFiles": true,
// Ensure these are always set regardless of base tsconfig
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
include: [safe_cypress_config_filepath]
};
} else {
// Scenario 2: No tsconfig or invalid tsconfig - create standalone with all basic parameters
tempTsConfig = {
compilerOptions: {
// Preserve old command-line parameters for backwards compatibility
"outDir": path.basename(complied_js_dir),
"listEmittedFiles": true,
"allowSyntheticDefaultImports": true,
"module": "commonjs",
"declaration": false,

// Add essential missing parameters for robust compilation
"target": "es2017",
"moduleResolution": "node",
"esModuleInterop": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": false, // Avoid breaking existing code
"noEmitOnError": false // Continue compilation even with errors
},
include: [safe_cypress_config_filepath],
exclude: ["node_modules", "dist", "build"]
};
}

// Write the temporary tsconfig
const tempTsConfigPath = path.join(safe_working_dir, 'tsconfig.singlefile.tmp.json');

Check warning

Code scanning / Semgrep OSS

Semgrep Finding: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal Warning

Detected possible user input going into a path.join or path.resolve function. This could possibly lead to a path traversal vulnerability, where the attacker can access arbitrary files stored in the file system. Instead, be sure to sanitize or validate user input first.
fs.writeFileSync(tempTsConfigPath, JSON.stringify(tempTsConfig, null, 2));
logger.info(`Temporary tsconfig created at: ${tempTsConfigPath}`);

// Platform-specific command generation with sanitized paths
const isWindows = /^win/.test(process.platform);

if (isWindows) {
// Windows: Use && to chain commands, no space after SET
const setNodePath = `set NODE_PATH=${safe_bstack_node_modules_path}`;
const tscCommand = `${setNodePath} && node "${typescript_path}" --project "${tempTsConfigPath}" && ${setNodePath} && node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
logger.info(`TypeScript compilation command: ${tscCommand}`);
return { tscCommand, tempTsConfigPath };
} else {
// Unix/Linux/macOS: Use ; to separate commands or && to chain
const nodePathPrefix = `NODE_PATH=${safe_bstack_node_modules_path}`;
const tscCommand = `${nodePathPrefix} node "${typescript_path}" --project "${tempTsConfigPath}" && ${nodePathPrefix} node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
logger.info(`TypeScript compilation command: ${tscCommand}`);
return { tscCommand, tempTsConfigPath };
}
}

exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_modules_path) => {
const cypress_config_filename = bsConfig.run_settings.cypress_config_filename
const working_dir = path.dirname(cypress_config_filepath);
Expand All @@ -22,19 +175,12 @@
}
fs.mkdirSync(complied_js_dir, { recursive: true })

const typescript_path = path.join(bstack_node_modules_path, 'typescript', 'bin', 'tsc')
const { tscCommand, tempTsConfigPath } = generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, complied_js_dir, cypress_config_filepath);

let tsc_command = `NODE_PATH=${bstack_node_modules_path} node "${typescript_path}" --outDir "${complied_js_dir}" --listEmittedFiles true --allowSyntheticDefaultImports --module commonjs --declaration false "${cypress_config_filepath}"`

if (/^win/.test(process.platform)) {
tsc_command = `set NODE_PATH=${bstack_node_modules_path}&& node "${typescript_path}" --outDir "${complied_js_dir}" --listEmittedFiles true --allowSyntheticDefaultImports --module commonjs --declaration false "${cypress_config_filepath}"`
}


let tsc_output
try {
logger.debug(`Running: ${tsc_command}`)
tsc_output = cp.execSync(tsc_command, { cwd: working_dir })
logger.debug(`Running: ${tscCommand}`)
tsc_output = cp.execSync(tscCommand, { cwd: working_dir })
} catch (err) {
// error while compiling ts files
logger.debug(err.message);
Expand All @@ -44,6 +190,21 @@
logger.debug(`Saved compiled js output at: ${complied_js_dir}`);
logger.debug(`Finding compiled cypress config file in: ${complied_js_dir}`);

// Clean up the temporary tsconfig file
if (fs.existsSync(tempTsConfigPath)) {
fs.unlinkSync(tempTsConfigPath);
logger.debug(`Temporary tsconfig file removed: ${tempTsConfigPath}`);
}

if (tsc_output) {
logger.debug(tsc_output.toString());
}

if (!tsc_output) {
logger.error('No TypeScript compilation output available');
return null;
}

const lines = tsc_output.toString().split('\n');
let foundLine = null;
for (let i = 0; i < lines.length; i++) {
Expand All @@ -53,7 +214,7 @@
}
}
if (foundLine === null) {
logger.error(`No compiled cypress config found. There might some error running ${tsc_command} command`)
logger.error(`No compiled cypress config found. There might some error running ${tscCommand} command`)
return null
} else {
const compiled_cypress_config_filepath = foundLine.split('TSFILE: ').pop()
Expand Down
Loading
Loading