Skip to content

CLI-541 Add dependency risk analysis git push hook#339

Closed
georgii-borovinskikh-sonarsource wants to merge 1 commit into
masterfrom
gb/sca-git-poc
Closed

CLI-541 Add dependency risk analysis git push hook#339
georgii-borovinskikh-sonarsource wants to merge 1 commit into
masterfrom
gb/sca-git-poc

Conversation

@georgii-borovinskikh-sonarsource
Copy link
Copy Markdown
Contributor

@georgii-borovinskikh-sonarsource georgii-borovinskikh-sonarsource commented May 29, 2026


Summary by Gitar

  • Dependency risk scanning:
    • Added --with-dependency-risks flag to integrate git to enable automated pre-push analysis, blocking pushes with detected risks.
    • Implemented ScaScanOrchestrator and ScaWatchPatternsRunner using picomatch to manage project scanning, version verification, and targeted manifest analysis.
  • Binary lifecycle management:
    • Registered sonarScaScannerBinaryDependency and integrated updateScaScannerBinaryIfNeeded into the post-update workflow for automated updates.
  • Git integration enhancements:
    • Refactored hook installers to support multi-stage execution and project-specific configurations for pre-push handlers.
    • Added support for legacy markers to ensure smooth upgrades of existing hook installations.
  • Testing:
    • Added comprehensive integration tests covering dependency risk workflows, interactive prompts, and server-side validation (e.g., version checks, SCA availability).
    • Added unit tests for scan orchestration, watch pattern matching, and project key validation.

This will update automatically on new commits.

@hashicorp-vault-sonar-prod
Copy link
Copy Markdown

hashicorp-vault-sonar-prod Bot commented May 29, 2026

CLI-541

Comment thread src/cli/commands/integrate/git/tools/husky/shell-fragments.ts Outdated
Comment thread src/cli/commands/hook/sca-watch-patterns.ts Outdated
@georgii-borovinskikh-sonarsource georgii-borovinskikh-sonarsource marked this pull request as ready for review June 1, 2026 10:18
Comment thread src/cli/commands/integrate/git/index.ts Outdated
Comment thread src/cli/commands/hook/git-pre-push-dependency-risks.ts Outdated
Comment on lines +90 to +95
for (const legacyMarker of this.options.legacyStartMarkers ?? []) {
const legacyIndex = existing.indexOf(legacyMarker);
if (legacyIndex >= 0) {
return `${existing.slice(0, legacyIndex)}${managedBlock}\n`;
}
}
Copy link
Copy Markdown

@gitar-bot gitar-bot Bot Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Legacy marker upgrade discards user content after managed block

When renderContent encounters a legacy start marker (line 90-94), it truncates the file at that position and appends the new managed block. If the user has appended their own scripts after the old end marker (e.g. npm test after # sonar:end husky-pre-push), that content is silently lost.

This is a realistic scenario because husky hook files commonly contain multiple tool invocations. The existing regex-based replacement on line 81-82 correctly does in-place replacement preserving surrounding content, but the legacy fallback doesn't attempt to find the old end marker.

A fix would be to also try matching a regex with legacy start marker + current end marker before falling back to the slice approach.

Try regex replacement with legacy start + current end markers first (preserves content after block), fall back to slice only when no end marker is found.:

    for (const legacyMarker of this.options.legacyStartMarkers ?? []) {
      const legacyPattern = new RegExp(
        String.raw`${escapeRegExp(legacyMarker)}[\s\S]*?${escapeRegExp(this.endMarker)}`,
      );
      if (legacyPattern.test(existing)) {
        return existing.replace(legacyPattern, managedBlock);
      }
      const legacyIndex = existing.indexOf(legacyMarker);
      if (legacyIndex >= 0) {
        return `${existing.slice(0, legacyIndex)}${managedBlock}
`;
      }
    }

Was this helpful? React with 👍 / 👎

Comment on lines +260 to +266
if (options.nonInteractive) validateProjectKeyOption('--project', options.project);
validateDependencyRisksOption(options);
if (options.withDependencyRisks) {
throw new InvalidOptionError(
'--with-dependency-risks is not supported with --global; install per project instead',
);
}
Copy link
Copy Markdown

@gitar-bot gitar-bot Bot Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: Global validation order gives misleading error message

In integrateGitGlobal, validateDependencyRisksOption(options) is called before the --global incompatibility check. When a user passes --global --with-dependency-risks --non-interactive without --project, they get "--with-dependency-risks requires -p " instead of the more helpful "--with-dependency-risks is not supported with --global".

Swap the order so the global-specific check runs first.

Move the --global incompatibility check before the generic dependency-risks validation.:

validateHookOption(options.hook);
if (options.withDependencyRisks) {
  throw new InvalidOptionError(
    '--with-dependency-risks is not supported with --global; install per project instead',
  );
}
if (options.nonInteractive) validateProjectKeyOption('--project', options.project);
validateDependencyRisksOption(options);

Was this helpful? React with 👍 / 👎

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 2, 2026

@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Jun 3, 2026

Code Review ⚠️ Changes requested 4 resolved / 6 findings

Integrates automated dependency risk analysis into the Git push workflow with new orchestration logic and update mechanisms. The legacy marker upgrade logic currently truncates user content, and global validation order triggers incorrect error messages.

⚠️ Bug: Legacy marker upgrade discards user content after managed block

📄 src/cli/commands/integrate/_common/registry/resources/text-snippet.ts:90-95

When renderContent encounters a legacy start marker (line 90-94), it truncates the file at that position and appends the new managed block. If the user has appended their own scripts after the old end marker (e.g. npm test after # sonar:end husky-pre-push), that content is silently lost.

This is a realistic scenario because husky hook files commonly contain multiple tool invocations. The existing regex-based replacement on line 81-82 correctly does in-place replacement preserving surrounding content, but the legacy fallback doesn't attempt to find the old end marker.

A fix would be to also try matching a regex with legacy start marker + current end marker before falling back to the slice approach.

Try regex replacement with legacy start + current end markers first (preserves content after block), fall back to slice only when no end marker is found.
    for (const legacyMarker of this.options.legacyStartMarkers ?? []) {
      const legacyPattern = new RegExp(
        String.raw`${escapeRegExp(legacyMarker)}[\s\S]*?${escapeRegExp(this.endMarker)}`,
      );
      if (legacyPattern.test(existing)) {
        return existing.replace(legacyPattern, managedBlock);
      }
      const legacyIndex = existing.indexOf(legacyMarker);
      if (legacyIndex >= 0) {
        return `${existing.slice(0, legacyIndex)}${managedBlock}
`;
      }
    }
💡 Bug: Global validation order gives misleading error message

📄 src/cli/commands/integrate/git/index.ts:260-266

In integrateGitGlobal, validateDependencyRisksOption(options) is called before the --global incompatibility check. When a user passes --global --with-dependency-risks --non-interactive without --project, they get "--with-dependency-risks requires -p " instead of the more helpful "--with-dependency-risks is not supported with --global".

Swap the order so the global-specific check runs first.

Move the --global incompatibility check before the generic dependency-risks validation.
validateHookOption(options.hook);
if (options.withDependencyRisks) {
  throw new InvalidOptionError(
    '--with-dependency-risks is not supported with --global; install per project instead',
  );
}
if (options.nonInteractive) validateProjectKeyOption('--project', options.project);
validateDependencyRisksOption(options);
✅ 4 resolved
Edge Case: watch-patterns JSON parsing may fail on trailing output

📄 src/cli/commands/hook/sca-watch-patterns.ts:43-50 📄 src/cli/commands/hook/sca-watch-patterns.ts:46-52
In getScaWatchPatterns, the code finds the first { in stdout and passes stdout.slice(jsonStart) to JSON.parse. If the sca-scanner binary emits any text after the JSON object (e.g., a trailing newline with log messages, or multiple JSON objects), JSON.parse will throw a SyntaxError. While the outer try/catch will handle this gracefully (returning []), it means the hook silently skips the dependency-risks scan even when valid patterns are available.

Additionally, if non-JSON output (like a log line) contains a { character before the actual JSON payload, parsing will fail or produce wrong results.

Security: Shell injection via unsanitized project key in hook scripts

📄 src/cli/commands/integrate/git/tools/husky/shell-fragments.ts:44 📄 src/cli/commands/integrate/git/tools/native/shell-fragments.ts:41 📄 src/cli/commands/integrate/git/index.ts:117-119 📄 src/cli/commands/integrate/git/index.ts:151 📄 src/cli/commands/integrate/git/index.ts:115-125
The dependencyRisksProject value is interpolated directly into shell script content using single quotes (-p '${options.dependencyRisksProject}' --sca) in both the husky and native shell fragment generators. A project key containing a single quote (e.g., my'project) would break out of the single-quoted string and allow arbitrary shell command execution when the git hook runs.

The only validation performed is checking that the trimmed value is non-empty (v.trim().length > 0). There is no sanitization or rejection of shell metacharacters before writing to the hook file.

Bug: resolveProjectKey doesn't trim prompted value

📄 src/cli/commands/integrate/git/index.ts:172-176
The resolveProjectKey function validates the prompted input with v.trim() (line 174) but returns the raw value from promptUntilValid without trimming (line 172). The old resolveDependencyRisksProject code did value.trim() before returning. If a user enters a key with leading/trailing whitespace, it will pass validation but the untrimmed value propagates to the hook script and display message.

Quality: Uncommitted-changes warning shown even when scan is skipped

📄 src/cli/commands/hook/git-pre-push-dependency-risks.ts:69-78
In runDepRisksStage, the hasUncommittedChanges() check and its warning are emitted before verifying whether any dependency manifests actually changed. If no manifests changed, the function prints a misleading warning about uncommitted changes and then immediately prints 'No dependency manifests changed … skipping'. The warning should be moved after the anyFileMatches check so it only appears when a scan will actually run.

🤖 Prompt for agents
Code Review: Integrates automated dependency risk analysis into the Git push workflow with new orchestration logic and update mechanisms. The legacy marker upgrade logic currently truncates user content, and global validation order triggers incorrect error messages.

1. ⚠️ Bug: Legacy marker upgrade discards user content after managed block
   Files: src/cli/commands/integrate/_common/registry/resources/text-snippet.ts:90-95

   When `renderContent` encounters a legacy start marker (line 90-94), it truncates the file at that position and appends the new managed block. If the user has appended their own scripts *after* the old end marker (e.g. `npm test` after `# sonar:end husky-pre-push`), that content is silently lost.
   
   This is a realistic scenario because husky hook files commonly contain multiple tool invocations. The existing regex-based replacement on line 81-82 correctly does in-place replacement preserving surrounding content, but the legacy fallback doesn't attempt to find the old end marker.
   
   A fix would be to also try matching a regex with legacy start marker + current end marker before falling back to the slice approach.

   Fix (Try regex replacement with legacy start + current end markers first (preserves content after block), fall back to slice only when no end marker is found.):
       for (const legacyMarker of this.options.legacyStartMarkers ?? []) {
         const legacyPattern = new RegExp(
           String.raw`${escapeRegExp(legacyMarker)}[\s\S]*?${escapeRegExp(this.endMarker)}`,
         );
         if (legacyPattern.test(existing)) {
           return existing.replace(legacyPattern, managedBlock);
         }
         const legacyIndex = existing.indexOf(legacyMarker);
         if (legacyIndex >= 0) {
           return `${existing.slice(0, legacyIndex)}${managedBlock}
   `;
         }
       }

2. 💡 Bug: Global validation order gives misleading error message
   Files: src/cli/commands/integrate/git/index.ts:260-266

   In `integrateGitGlobal`, `validateDependencyRisksOption(options)` is called before the `--global` incompatibility check. When a user passes `--global --with-dependency-risks --non-interactive` without `--project`, they get "--with-dependency-risks requires -p <project>" instead of the more helpful "--with-dependency-risks is not supported with --global".
   
   Swap the order so the global-specific check runs first.

   Fix (Move the --global incompatibility check before the generic dependency-risks validation.):
   validateHookOption(options.hook);
   if (options.withDependencyRisks) {
     throw new InvalidOptionError(
       '--with-dependency-risks is not supported with --global; install per project instead',
     );
   }
   if (options.nonInteractive) validateProjectKeyOption('--project', options.project);
   validateDependencyRisksOption(options);

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.
Unblock → Override a blocking verdict and allow merging.

Comment with these commands to change:

Auto-apply Compact Unblock
gitar auto-apply:on         
gitar display:verbose         
gitar unblock         

Was this helpful? React with 👍 / 👎 | Gitar

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