Skip to content

Unify planning pipeline + harden semver/lockfile analyzers#11

Open
pmclSF wants to merge 36 commits intomainfrom
feat/review-next
Open

Unify planning pipeline + harden semver/lockfile analyzers#11
pmclSF wants to merge 36 commits intomainfrom
feat/review-next

Conversation

@pmclSF
Copy link
Copy Markdown
Owner

@pmclSF pmclSF commented Mar 2, 2026

Summary

  • unify plan/merge/server planning flow around a shared buildApplyPlan core module
  • route CLI plan and server runPlan through the shared builder
  • route non-history merge through plan+apply to eliminate duplicated pipeline logic
  • harden semver parsing/comparison and peer range checks with safer normalization
  • improve lockfile parsing robustness for pnpm/yarn/npm formats and emit parse warnings
  • surface verify-check parsing errors with detailed failure context
  • add UI page-level integration tests for merge and verify flows

Validation

  • pnpm build
  • pnpm exec vitest run tests/e2e/cli.test.ts tests/integration/plan-apply.test.ts tests/integration/prepare-plan-apply.test.ts tests/integration/verify-command.test.ts
  • pnpm exec vitest run tests/unit/analyzers/lockfile.test.ts tests/unit/analyzers/peers.test.ts tests/unit/analyzers/dependencies.edge-cases.test.ts tests/analyzers/dependencies.test.ts tests/unit/commands/verify-checks.test.ts tests/integration/verify-command.test.ts
  • cd ui && pnpm test
  • pnpm run build:ui

pmclSF and others added 28 commits February 28, 2026 04:28
…s, and UI wizard

Add 12 stages of functionality including:
- Stage 9/17: Enhanced 8-step wizard UI with SeverityBadge, DiffViewer, TreePreview,
  FindingsFilter components and deeper page integration
- Stage 10: History preservation prerequisites and dry-run reporting
- Stage 11: Full lifecycle CLI commands (add, archive, migrate-branch) with plan types
- Stage 12: Extended analysis (environment, tooling, CI, publishing, repo risks) with
  risk classification
- Stage 13: Path-filtered GitHub Actions workflow generation
- Stage 14: Configure engine for Prettier/ESLint/TypeScript scaffolding
- Stage 15: Dependency enforcement via overrides/resolutions
- Stage 16: Cross-platform path normalization
- Stage 18: Smart defaults with evidence-based suggestions and error shaping
- Stage 19: Multi-language detection and scaffolding (Go, Rust, Python)
- Stage 20: Performance utilities (pMap concurrency, disk space check, progress emitter)

674 unit tests passing across 49 test files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ckers

SEC-01: Sanitize Python injection in history-preserve commitPrefix
SEC-02: Add path traversal prevention in apply command
SEC-03: Add Bearer token auth to server API and WebSocket
SEC-04: Allowlist install command executables in apply
SEC-05: Replace shell exec() with execFile() in ui command
SEC-06: Cap concurrent operations and event buffer in WsHub

Publishing: rename to monotize, add LICENSE/SECURITY.md/CHANGELOG.md,
add files field, author, repository metadata, semver and js-yaml deps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

// Fetch
logger.info(`Fetching ${branch}...`);
await safeExecFile('git', ['fetch', remoteName, branch], {

Check failure

Code scanning / CodeQL

Second order command injection High

Command line argument that depends on
a user-provided value
can execute an arbitrary command if --upload-pack is used with git.

Copilot Autofix

AI 25 days ago

At a high level, the fix is to validate and constrain the user-provided branch (and related path-like values) before they are used in git commands or as filesystem subpaths. We should accept only syntactically valid git ref names, reject anything containing dangerous characters or starting with - (which could be interpreted as an option), and ensure there is no path traversal (..) or embedded whitespace. This keeps the application’s behavior the same for legitimate inputs while blocking malicious ones.

The best single approach here, without changing overall functionality, is:

  1. Introduce small helper functions in src/strategies/migrate-branch.ts to validate:

    • branch as a git ref name (no leading -, no whitespace, no control characters, no .., and excluding characters disallowed by git in ref names such as ~, ^, :, ?, *, [, \, and .. segments).
    • subdir (which ultimately comes from options.subdir or plan.sourceRepo) as a safe relative directory or absolute path without .. segments when used as a --prefix or --directory argument.
  2. Call these validators early in generateBranchPlan (for branch) so that the BranchPlan itself is always safe, and again in applyBranchPlan (for the effective subdir value, which is not part of the plan but is derived from request options).

  3. If validation fails, throw an error with a clear message; this will be caught by the existing try/catch in the route and reported to the client as an error, preserving the overall error-handling flow.

Concretely:

  • In src/strategies/migrate-branch.ts, above checkBranchMigratePrerequisites, add two helper functions validateBranchName(branch: string): void and validateSubdir(subdir: string): void that perform the above checks and throw an Error on invalid input.
  • In generateBranchPlan, immediately after receiving the branch parameter (around lines 105–107), call validateBranchName(branch);.
  • In applyBranchPlan, before dispatching to applySubtreeImport/applyPatchReplay, call validateSubdir(subdir);.
  • Optionally, in the same file, we can reuse validateBranchName for any other git-branch uses (branchMigrateDryRun), but the critical path highlighted by CodeQL is generateBranchPlanapplyBranchPlan → git commands.

No external dependencies are required; we can implement the validation with simple regexes and string checks.


Suggested changeset 1
src/strategies/migrate-branch.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/strategies/migrate-branch.ts b/src/strategies/migrate-branch.ts
--- a/src/strategies/migrate-branch.ts
+++ b/src/strategies/migrate-branch.ts
@@ -4,6 +4,53 @@
 import { pathExists } from '../utils/fs.js';
 
 /**
+ * Validate that a branch name is a safe git ref (no options, no traversal, no illegal characters).
+ * Throws an Error if the branch name is invalid.
+ */
+function validateBranchName(branch: string): void {
+  // Disallow empty, whitespace, or leading dash (to avoid option-like values).
+  if (!branch || typeof branch !== 'string' || /^\s/.test(branch) || /\s/.test(branch) || branch.startsWith('-')) {
+    throw new Error(`Invalid branch name: ${branch}`);
+  }
+
+  // Disallow path traversal and double dots which are problematic in ref names.
+  if (branch.includes('..')) {
+    throw new Error(`Invalid branch name (contains ".."): ${branch}`);
+  }
+
+  // Git ref name must not contain these characters: space, ~, ^, :, ?, *, [, \, or control chars.
+  if (/[~^:?*\[\\]/.test(branch)) {
+    throw new Error(`Invalid branch name (contains illegal characters): ${branch}`);
+  }
+
+  // Disallow trailing slash or multiple consecutive slashes.
+  if (branch.endsWith('/') || branch.includes('//')) {
+    throw new Error(`Invalid branch name (invalid slash usage): ${branch}`);
+  }
+}
+
+/**
+ * Validate that a subdirectory path used for subtree/am is safe.
+ * Throws an Error if the path is invalid.
+ */
+function validateSubdir(subdir: string): void {
+  if (!subdir || typeof subdir !== 'string') {
+    throw new Error(`Invalid subdir: ${subdir}`);
+  }
+
+  // Prevent option-like values.
+  if (subdir.startsWith('-')) {
+    throw new Error(`Invalid subdir (looks like an option): ${subdir}`);
+  }
+
+  // Normalize and ensure no path traversal via ".." segments.
+  const normalized = path.normalize(subdir);
+  if (normalized.split(path.sep).includes('..')) {
+    throw new Error(`Invalid subdir (path traversal not allowed): ${subdir}`);
+  }
+}
+
+/**
  * Check prerequisites for branch migration
  */
 export async function checkBranchMigratePrerequisites(
@@ -102,6 +149,9 @@
   strategy: BranchMigrateStrategy,
   logger: Logger,
 ): Promise<BranchPlan> {
+  // Ensure the provided branch name is a safe git reference before using it anywhere.
+  validateBranchName(branch);
+
   const srcPath = path.resolve(sourceRepo);
   const targetPath = path.resolve(targetMonorepo);
 
@@ -279,6 +329,9 @@
   subdir: string,
   logger: Logger,
 ): Promise<void> {
+  // Validate subdir before using it in git command arguments (e.g., --prefix, --directory).
+  validateSubdir(subdir);
+
   if (plan.strategy === 'subtree') {
     await applySubtreeImport(plan, subdir, logger);
   } else {
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
// Use wmic on Windows
const drive = path.parse(path.resolve(dirPath)).root;
const { stdout } = await execFileAsync('wmic', [
'logicaldisk', 'where', `DeviceID='${drive.replace('\\', '')}'`,

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This replaces only the first occurrence of '\'.

Copilot Autofix

AI 25 days ago

In general, to fix this kind of issue you should either use a regular expression with the global (g) flag or use a utility that clearly operates on all occurrences (for example, split/join). This ensures that every instance of the character you intend to escape or remove is handled, not just the first one.

For this specific case, the safest and clearest fix is to remove all backslashes from drive before embedding it in the wmic query. A minimal change that preserves existing functionality is to replace drive.replace('\\', '') with a global replacement, e.g. drive.replace(/\\/g, ''). This removes all \ characters and matches the apparent intent of producing a device ID like C:.

Only one line in src/utils/disk.ts needs to change: line 18, where drive.replace('\\', '') appears. No new imports or helper methods are required.

Suggested changeset 1
src/utils/disk.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/utils/disk.ts b/src/utils/disk.ts
--- a/src/utils/disk.ts
+++ b/src/utils/disk.ts
@@ -15,7 +15,7 @@
       // Use wmic on Windows
       const drive = path.parse(path.resolve(dirPath)).root;
       const { stdout } = await execFileAsync('wmic', [
-        'logicaldisk', 'where', `DeviceID='${drive.replace('\\', '')}'`,
+        'logicaldisk', 'where', `DeviceID='${drive.replace(/\\/g, '')}'`,
         'get', 'FreeSpace', '/format:value',
       ]);
       const match = stdout.match(/FreeSpace=(\d+)/);
EOF
@@ -15,7 +15,7 @@
// Use wmic on Windows
const drive = path.parse(path.resolve(dirPath)).root;
const { stdout } = await execFileAsync('wmic', [
'logicaldisk', 'where', `DeviceID='${drive.replace('\\', '')}'`,
'logicaldisk', 'where', `DeviceID='${drive.replace(/\\/g, '')}'`,
'get', 'FreeSpace', '/format:value',
]);
const match = stdout.match(/FreeSpace=(\d+)/);
Copilot is powered by AI and may make mistakes. Always verify output.
function extractRepoName(input: string, type: RepoSourceType): string {
if (type === 'local') {
// Handle POSIX, Windows drive, and UNC-style local paths.
const withoutTrailingSlash = input.replace(/[\\/]+$/, '');

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
a user-provided value
may run slow on strings with many repetitions of '/'.
This
regular expression
that depends on
a user-provided value
may run slow on strings with many repetitions of '/'.
This
regular expression
that depends on
a user-provided value
may run slow on strings with many repetitions of '/'.
This
regular expression
that depends on
a user-provided value
may run slow on strings with many repetitions of '/'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant