Skip to content
Open
2 changes: 1 addition & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
fileignoreconfig:
- filename: pnpm-lock.yaml
checksum: 07642e8dd04d580185a459e5b088d8a1bb4e91be4e04f4842bf4fe4775205bf6
checksum: 96e1bd3333f28545394ad65b791405087afa5b5cdc9da9e587bd9f9d00e89ee9
version: '1.0'
4 changes: 2 additions & 2 deletions packages/contentstack-apps-cli/src/commands/app/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default class Install extends AppCLIBaseCommand {
description: commonMsg.APP_UID,
}),
"stack-api-key": flags.string({
description: commonMsg.STACK_API_KEY,
description: commonMsg.STACK_API_KEY_placeholder,
}),
...AppCLIBaseCommand.baseFlags,
};
Expand Down Expand Up @@ -88,7 +88,7 @@ export default class Install extends AppCLIBaseCommand {
// in case a stack app is selected and no stack-api-key is provided
if (appType === "stack" && !this.flags["stack-api-key"]) {
this.log(
$t(installAppMsg.MISSING_STACK_API_KEY, {
$t(installAppMsg.MISSING_STACK_API_KEY_placeholder, {
app: app?.name || app?.uid,
}),
"warn"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class Reinstall extends AppCLIBaseCommand {
description: commonMsg.APP_UID,
}),
"stack-api-key": flags.string({
description: commonMsg.STACK_API_KEY,
description: commonMsg.STACK_API_KEY_placeholder,
}),
...AppCLIBaseCommand.baseFlags,
};
Expand Down Expand Up @@ -84,7 +84,7 @@ export default class Reinstall extends AppCLIBaseCommand {
appType = "stack";

this.log(
$t(reinstallAppMsg.MISSING_STACK_API_KEY, {
$t(reinstallAppMsg.MISSING_STACK_API_KEY_placeholder, {
app: app?.name || app?.uid,
}),
"warn"
Expand Down
6 changes: 3 additions & 3 deletions packages/contentstack-apps-cli/src/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const commonMsg = {
APP_UID: "Provide the app UID of an existing app.",
APP_TYPE_DESCRIPTION: "Type of app",
CONTACT_SUPPORT: "Contact the support team for help.",
STACK_API_KEY: "API key of the stack where the app operation is to be performed.",
STACK_API_KEY_placeholder: "API key of the stack where the app operation is to be performed.",
USER_TERMINATION: "Process terminated by the user.",
CLI_APP_CLI_LOGIN_FAILED: "You're not logged in. Run $ csdx auth:login to continue."
};
Expand Down Expand Up @@ -97,7 +97,7 @@ const installAppMsg = {
CHOOSE_A_STACK: "Please select a stack",
APP_INSTALLED_SUCCESSFULLY: "{app} installed successfully in {target}.",
INSTALL_ORG_APP_TO_STACK: "{app} is an organization app. It cannot be installed to a stack. Do you want to proceed?",
MISSING_STACK_API_KEY: "As {app} is a stack app, it can only be installed in a stack. Please select a stack.",
MISSING_STACK_API_KEY_placeholder: "As {app} is a stack app, it can only be installed in a stack. Please select a stack.",
INSTALLING_APP_NOTICE: "Installing {app} on {type} {target}.",
APP_ALREADY_INSTALLED: "Please use $ csdx app:reinstall to reinstall the app.",
}
Expand All @@ -115,7 +115,7 @@ const reinstallAppMsg = {
CHOOSE_A_STACK: "Please select a stack",
APP_REINSTALLED_SUCCESSFULLY: "{app} reinstalled successfully in {target}.",
REINSTALL_ORG_APP_TO_STACK: "{app} is an organization app. It cannot be reinstalled to a stack. Do you want to proceed?",
MISSING_STACK_API_KEY: "As {app} is a stack app, it can only be reinstalled in a stack. Please select a stack.",
MISSING_STACK_API_KEY_placeholder: "As {app} is a stack app, it can only be reinstalled in a stack. Please select a stack.",
REINSTALLING_APP_NOTICE: "Reinstalling {app} on {type} {target}.",
APP_UID: "Provide the app UID of an existing app to be reinstalled.",
APP_ALREADY_LATEST_VERSION: "The application is already up to date; no new version is available.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
}),
'stack-api-key': flags.string({
char: 'k',
description: messages.STACK_API_KEY,
description: messages.STACK_API_KEY_placeholder,
}),
operation: flags.string({
description: messages.OPERATION,
Expand Down Expand Up @@ -124,8 +124,8 @@

protected managementStack!: ManagementStack;
protected deliveryStack!: DeliveryStack | null;
protected logger: any;

Check warning on line 127 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
protected loggerContext: any;

Check warning on line 128 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
protected bulkOperationConfig!: BulkOperationConfig;
protected bulkService!: BulkOperationService;
protected queueManager!: QueueManager;
Expand All @@ -133,7 +133,7 @@
protected retryStrategy!: RetryStrategy;
protected operationExecutor!: OperationExecutor;
private batchResults: Map<string, BulkJobResult> = new Map();
protected parsedFlags: any;

Check warning on line 136 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type

/**
* Initialize common components
Expand Down Expand Up @@ -184,7 +184,7 @@
* Initialize for revert/retry operations
* All configuration is loaded from the log file - no other flags needed
*/
private async initForRevertOrRetry(flags: any): Promise<void> {

Check warning on line 187 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
const logPath = flags.revert || flags['retry-failed'];
const isRetry = !!flags['retry-failed'];

Expand Down Expand Up @@ -230,7 +230,7 @@
let stackConfig;
try {
stackConfig = setupStackConfig(flags, this.cmaHost, this.cdaHost);
} catch (error: any) {

Check warning on line 233 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
throw error;
}

Expand All @@ -247,7 +247,7 @@
if (flags.branch && !flags.revert && !flags['retry-failed']) {
try {
await validateBranch(this.managementStack, flags.branch, this.logger);
} catch (error: any) {

Check warning on line 250 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
throw error;
}
}
Expand All @@ -259,7 +259,7 @@
// Store environment UIDs in config for filtering operations
this.bulkOperationConfig.environmentUids = environmentUids;
this.logger.debug(`Stored environment UIDs in config: ${environmentUids.join(', ')}`, this.loggerContext);
} catch (error: any) {

Check warning on line 262 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
throw error;
}
}
Expand All @@ -268,7 +268,7 @@
/**
* Build operation configuration
*/
protected async buildConfiguration(flags: any): Promise<void> {

Check warning on line 271 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
this.bulkOperationConfig = buildConfig(flags);

// buildConfig splits comma-separated oclif `multiple` values; mirror onto flags so
Expand Down Expand Up @@ -348,7 +348,7 @@
/**
* Show operation summary and get confirmation
*/
protected async confirmOperation(items?: any[]): Promise<boolean> {

Check warning on line 351 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
const flags = this.parsedFlags || (await this.parse(this.constructor as typeof BaseBulkCommand)).flags;
const itemCount = items?.length || 0;

Expand All @@ -362,7 +362,7 @@
* @param items - Items to process
* @returns Aggregated results from all operations
*/
protected async executeBulkOperation(items: any[]): Promise<BulkOperationResult> {

Check warning on line 365 in packages/contentstack-bulk-operations/src/base-bulk-command.ts

View workflow job for this annotation

GitHub Actions / run-tests

Unexpected any. Specify a different type
this.logger.debug($t(messages.EXECUTING_OPERATION, { count: items.length }), this.loggerContext);
const startTime = Date.now();

Expand Down
5 changes: 3 additions & 2 deletions packages/contentstack-bulk-operations/src/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ const interactiveMsg = {
ENTER_ENVIRONMENTS: 'Enter target environments (comma-separated):',
ENTER_LOCALES: 'Enter locales (comma-separated):',
SELECT_ALIAS: 'Select alias:',
ENTER_API_KEY: 'Enter stack API key:',
ENTER_API_KEY_placeholder: 'Enter stack API key:',
ENTER_SOURCE_ENV: 'Enter source environment name:',
SELECT_SOURCE_ALIAS: 'Select delivery token alias for source environment:',

Expand Down Expand Up @@ -322,7 +322,8 @@ const flagDescriptions = {
// Common flags
ALIAS:
'Uses the name of a saved Management Token to authenticate the command. The command can only access the branches allowed for that token. This option can be used as an alternative to` --stack-api-key.`',
STACK_API_KEY: 'API key of the source stack. You must use either the --stack-api-key flag or the --alias flag.',
STACK_API_KEY_placeholder:
'API key of the source stack. You must use either the --stack-api-key flag or the --alias flag.',
OPERATION: 'Specifies whether to `publish` or `unpublish` content.',
ENVIRONMENTS:
'Specifies one or more environments where the entries or assets should be published. Separate multiple environments with spaces.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async function promptForStackCredentials(): Promise<{ alias?: string; apiKey?: s
const apiKey = await cliux.inquire<string>({
type: 'input',
name: 'apiKey',
message: messages.ENTER_API_KEY,
message: messages.ENTER_API_KEY_placeholder,
validate: (value) => {
if (!value || value.trim().length === 0) {
return messages.API_KEY_REQUIRED;
Expand Down Expand Up @@ -97,7 +97,7 @@ async function promptForStackCredentials(): Promise<{ alias?: string; apiKey?: s
const apiKey = await cliux.inquire<string>({
type: 'input',
name: 'apiKey',
message: messages.ENTER_API_KEY,
message: messages.ENTER_API_KEY_placeholder,
validate: (value) => {
if (!value || value.trim().length === 0) {
return messages.API_KEY_REQUIRED;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('Messages', () => {

it('should export flag descriptions', () => {
expect(messages.ALIAS).to.be.a('string');
expect(messages.STACK_API_KEY).to.be.a('string');
expect(messages.STACK_API_KEY_placeholder).to.be.a('string');
expect(messages.OPERATION).to.be.a('string');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const messages = {
FLAG_ALIAS: 'Alias of the management token',
FLAG_ORG: 'Provide organization UID to clone org users',
FLAG_STACK_NAME: 'Name of the stack that needs to be created as CSV filename',
FLAG_STACK_API_KEY: 'Provide the stack API key of the source stack',
FLAG_STACK_API_KEY_placeholder: 'Provide the stack API key of the source stack',
FLAG_ORG_NAME: 'Name of the organization that needs to be created as CSV filename',
FLAG_LOCALE: 'Locale of entries that will be exported',
FLAG_CONTENT_TYPE: 'Content type of entries that will be exported',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import contentfulValidator from './validator';
import { initContentfulMigrateConfig } from '../../services/contentful/config';
import { pickMasterLocale } from '../../services/contentful/prompts/master-locale';
import { writeMapper, type MapperBundle } from '../../services/contentful/mapper/write';
import { sanitizePath, pathValidator } from '../../lib/helpers';
import {
done,
logStageFail,
Expand Down Expand Up @@ -308,8 +309,9 @@ function enforceLocaleFallbacks(bundleRoot: string, masterCode: string): void {
const languageFile = path.join(localesDir, 'language.json');

const patch = (file: string, isMaster: boolean) => {
if (!fs.existsSync(file)) return;
const data = parseJsonLoose(fs.readFileSync(file, 'utf8'));
const safeFile = pathValidator(sanitizePath(file));
if (!fs.existsSync(safeFile)) return;
const data = parseJsonLoose(fs.readFileSync(safeFile, 'utf8'));
for (const k of Object.keys(data || {})) {
if (!data[k]) continue;
if (isMaster || data[k].code === masterCode) {
Expand All @@ -318,7 +320,7 @@ function enforceLocaleFallbacks(bundleRoot: string, masterCode: string): void {
data[k].fallback_locale = masterCode;
}
}
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
fs.writeFileSync(safeFile, JSON.stringify(data, null, 2), 'utf8');
};

patch(masterFile, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { parseJsonLoose } from '../../lib/parse-json-loose';
import { clearStaleImportState } from '../../lib/clear-import-state';
import { localDateStamp } from '../../lib/local-date';
import { inferWorkspace, patchManifest, stackApiKeyPrefix, toWorkspaceRelative } from '../../lib/manifest';
import { sanitizePath, pathValidator } from '../../lib/helpers';

export default class MigrateCreate extends Command {
static description = 'Convert a source export, create a new stack in an organization, and import into it';
Expand Down Expand Up @@ -180,7 +181,9 @@ export default class MigrateCreate extends Command {
);
// Each space gets its own output subdir + uses the space name as the
// stack name; --stack-name is honored only for a single target.
const outputRoot = multiSpace ? path.join(flags.output, sanitizeBranchUid(sp.name) || sp.id) : flags.output;
const outputRoot = multiSpace
? pathValidator(path.join(sanitizePath(flags.output), sanitizeBranchUid(sp.name) || sanitizeBranchUid(sp.id)))
: flags.output;
try {
const res = await this.migrateSpace({
adapter,
Expand Down Expand Up @@ -259,7 +262,7 @@ export default class MigrateCreate extends Command {
for (const env of envs) {
const branch = env === 'master' ? 'main' : sanitizeBranchUid(env);
const exportResult = await adapter.export({
outputDir: path.join(outputRoot, sanitizeBranchUid(env)),
outputDir: pathValidator(path.join(sanitizePath(outputRoot), sanitizeBranchUid(env))),
spaceId: cfSpaceId,
managementToken: cfManagementToken,
environmentId: env,
Expand Down Expand Up @@ -298,7 +301,7 @@ export default class MigrateCreate extends Command {
const stats = { locales: 0, contentTypes: 0, entries: 0 };
for (const j of jobs) {
const branch = j.branch ?? 'main';
const outDir = multi ? path.join(outputRoot, branch) : outputRoot;
const outDir = multi ? pathValidator(path.join(sanitizePath(outputRoot), branch)) : outputRoot;
const result = await adapter.convert({
input: j.exportFile,
outputDir: outDir,
Expand All @@ -307,7 +310,7 @@ export default class MigrateCreate extends Command {
verbose: flags.verbose,
orgUid,
});
converted.push({ job: j, branch, bundleDir: result.bundleDir });
converted.push({ job: j, branch, bundleDir: pathValidator(sanitizePath(result.bundleDir)) });
stats.contentTypes += result.stats.contentTypes;
stats.entries += result.stats.entries;
stats.locales = Math.max(stats.locales, result.stats.locales);
Expand Down Expand Up @@ -481,7 +484,7 @@ export default class MigrateCreate extends Command {
delivery_token: creds.deliveryToken,
preview_token: creds.previewToken,
};
const metadataPath = path.join(mainBundleDir, 'metadata.json');
const metadataPath = pathValidator(path.join(sanitizePath(mainBundleDir), 'metadata.json'));
fs.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
this.log(`✓ Bundle metadata written: ${metadataPath}`);

Expand Down Expand Up @@ -721,7 +724,7 @@ function readContentfulCliToken(): string | undefined {
* with `default: true`). Falls back to the first locale. Throws if there are none.
*/
function detectMasterLocale(input: string): string {
const raw = fs.readFileSync(input, 'utf8');
const raw = fs.readFileSync(pathValidator(input), 'utf8');
let locales: Array<{ code?: string; default?: boolean }> = [];
try {
locales = parseJsonLoose(raw)?.locales ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path';
import { spawn } from 'child_process';
import axios from 'axios';
import { configHandler, authHandler } from '@contentstack/cli-utilities';
import { sanitizePath, pathValidator } from './helpers';

const AXIOS_TIMEOUT = 60 * 1000;
/** Shorter timeout for the post-import read/provision calls so a wrong host or
Expand Down Expand Up @@ -598,7 +599,7 @@ export async function ensureWebhooks(
const result: EnsureWebhooksResult = { total: 0, created: [], skipped: [], failed: [] };
let bundleWebhooks: Record<string, any> = {};
try {
const raw = fs.readFileSync(path.join(bundleDir, 'webhooks', 'webhooks.json'), 'utf8');
const raw = fs.readFileSync(pathValidator(path.join(sanitizePath(bundleDir), 'webhooks', 'webhooks.json')), 'utf8');
bundleWebhooks = JSON.parse(raw) as Record<string, any>;
} catch {
return result; // no webhooks in bundle
Expand Down Expand Up @@ -666,7 +667,7 @@ export async function ensureWebhooks(
/** Environment names from a bundle's environments/environments.json (fallback). */
function readBundleEnvironments(bundleDir: string): string[] {
try {
const raw = fs.readFileSync(path.join(bundleDir, 'environments', 'environments.json'), 'utf8');
const raw = fs.readFileSync(pathValidator(path.join(sanitizePath(bundleDir), 'environments', 'environments.json')), 'utf8');
const parsed = JSON.parse(raw) as Record<string, { name?: string }>;
return Object.values(parsed)
.map((e) => e?.name)
Expand Down
29 changes: 29 additions & 0 deletions packages/contentstack-external-migrate/src/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { normalize, resolve } from 'path';

/**
* Strip directory-traversal sequences from a path segment/string.
*
* Mirrors `@contentstack/cli-utilities`' `sanitizePath`, but is defined locally
* so static analysis (Snyk) can see the traversal-stripping regex and treat it
* as a sanitizer — imports from node_modules are opaque to the taint engine.
* Removes `../`/`..\` sequences and collapses repeated slashes.
*/
export const sanitizePath = (str: string): string => {
if (typeof str !== 'string') return '';

const decodedStr = decodeURIComponent(str);
return decodedStr
.replace(/^([/\\]){2,}/, './') // normalize leading duplicate slashes/backslashes
.replace(/[/\\]+/g, '/') // collapse repeated slashes/backslashes into one
.replace(/(\.\.(\/|\\|$))+/g, ''); // remove directory traversal (../ or ..\)
};

/**
* Resolve a path against the current working directory and strip any leading
* `..` segments, guaranteeing the result cannot escape the process root via
* traversal. Mirrors `@contentstack/cli-utilities`' `pathValidator`, defined
* locally for the same static-analysis reason as {@link sanitizePath}.
*/
export const pathValidator = (filePath: string): string => {
return normalize(resolve(process.cwd(), filePath)).replace(/^(\.\.(\/|\\|$))+/, '');
};
7 changes: 4 additions & 3 deletions packages/contentstack-external-migrate/src/lib/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs';
import path from 'path';
import { sanitizePath, pathValidator } from './helpers';

export const MANIFEST_FILENAME = 'migration-manifest.json';
export const MANIFEST_VERSION = 1;
Expand Down Expand Up @@ -46,7 +47,7 @@ export interface MigrationManifest {
}

export function manifestFilePath(workspace: string): string {
return path.join(path.resolve(workspace), MANIFEST_FILENAME);
return pathValidator(path.join(path.resolve(sanitizePath(workspace)), MANIFEST_FILENAME));
}

export function stackApiKeyPrefix(stackApiKey: string): string {
Expand Down Expand Up @@ -165,10 +166,10 @@ export async function writeManifest(
workspace: string,
manifest: MigrationManifest,
): Promise<void> {
const filePath = manifestFilePath(workspace);
const filePath = pathValidator(sanitizePath(manifestFilePath(workspace)));
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });

const tmpPath = `${filePath}.${process.pid}.tmp`;
const tmpPath = pathValidator(sanitizePath(`${filePath}.${process.pid}.tmp`));
await fs.promises.writeFile(tmpPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
await fs.promises.rename(tmpPath, filePath);
}
Expand Down
Loading
Loading