Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 25 additions & 2 deletions src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2700,9 +2700,11 @@
w.newLine();
w.write("setlocal enabledelayedexpansion");
w.newLine();
String verboseFlag = isSshVerboseEnabled() ? " -vvv" : "";
w.write("\"" + sshexe.getAbsolutePath()
+ "\" -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" "
+ getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* ");
+ getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + verboseFlag

Check warning on line 2706 in src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 2703-2706 are not covered by tests
+ " %* ");
w.newLine();
}
ssh.toFile().setExecutable(true, true);
Expand All @@ -2724,8 +2726,10 @@
w.newLine();
w.write("fi");
w.newLine();
String verboseFlag = isSshVerboseEnabled() ? " -vvv" : "";

Check warning on line 2729 in src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 2729 is only partially covered, one branch is missing
w.write("ssh -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" "
+ getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\"");
+ getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + verboseFlag
+ " \"$@\"");
w.newLine();
}
return createNonBusyExecutable(ssh);
Expand Down Expand Up @@ -2768,6 +2772,25 @@
return launchCommandIn(args, workDir, environment);
}

/**
* Safely check if SSH verbose mode is enabled.
* Returns false if Jenkins instance is not available (e.g., during tests).
*
* @return true if SSH verbose mode is enabled, false otherwise
*/
private boolean isSshVerboseEnabled() {
try {
jenkins.model.Jenkins instance = jenkins.model.Jenkins.getInstanceOrNull();
if (instance != null) {

Check warning on line 2784 in src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 2784 is only partially covered, one branch is missing
return GitHostKeyVerificationConfiguration.get().isSshVerbose();
}
} catch (Exception e) {
// If we can't get the configuration, default to false
LOGGER.log(Level.FINE, "Unable to get SSH verbose configuration", e);

Check warning on line 2789 in src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 2785-2789 are not covered by tests
}
return false;
}

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

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

isSshVerboseEnabled() checks Jenkins.getInstanceOrNull() and returns false when it is null. CliGitAPIImpl methods execute on agents (the GitClient is created via MasterToSlaveFileCallable in Git#getClient), where Jenkins.getInstanceOrNull() is typically null. As a result, the -vvv flag will never be added for most real jobs running on agents, even when the global configuration enables it. Consider resolving the sshVerbose setting on the controller (where the GlobalConfiguration is available) and passing it to the agent along with hostKeyFactory (e.g., add a boolean to the callable and store it in the remote GitClient/CliGitAPIImpl instance).

Copilot uses AI. Check for mistakes.

private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars env)
throws GitException, InterruptedException {
return launchCommandIn(args, workDir, environment, TIMEOUT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

private SshHostKeyVerificationStrategy<? extends HostKeyVerifierFactory> sshHostKeyVerificationStrategy;

private boolean sshVerbose = false;

@Override
public @NonNull GlobalConfigurationCategory getCategory() {
return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
Expand All @@ -35,6 +37,29 @@
save();
}

/**
* Check if SSH verbose mode is enabled.
* When enabled, SSH commands will include -vvv flag for detailed diagnostic output.
* This helps troubleshoot SSH connection issues without requiring GIT_SSH_COMMAND environment variable.
*
* @return true if SSH verbose mode is enabled, false otherwise
*/
public boolean isSshVerbose() {
return sshVerbose;
}

/**
* Set SSH verbose mode.
* When enabled, SSH commands will include -vvv flag for detailed diagnostic output.
* This helps troubleshoot SSH connection issues without requiring GIT_SSH_COMMAND environment variable.
*
* @param sshVerbose true to enable SSH verbose mode, false to disable
*/
public void setSshVerbose(boolean sshVerbose) {
this.sshVerbose = sshVerbose;
save();
}

Check warning on line 61 in src/main/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 48-61 are not covered by tests

public static @NonNull GitHostKeyVerificationConfiguration get() {
return GlobalConfiguration.all().getInstance(GitHostKeyVerificationConfiguration.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:section title="Git Host Key Verification Configuration">
<f:dropdownDescriptorSelector field="sshHostKeyVerificationStrategy" title="Host Key Verification Strategy"/>
<f:entry title="SSH Verbose Mode" field="sshVerbose">
<f:checkbox title="Enable verbose SSH output for diagnostics"
help="When enabled, SSH commands will include -vvv flag for detailed diagnostic output. This helps troubleshoot SSH connection issues without requiring the GIT_SSH_COMMAND environment variable."/>

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

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

The checkbox help text is provided via a help attribute on <f:checkbox>, but Jenkins' form controls typically use a separate help-<field>.html (or help on <f:entry>) like the existing help-sshHostKeyVerificationStrategy.html. As written, this long string may not be displayed as intended and is harder to maintain/translate. Consider adding help-sshVerbose.html and referencing it from the <f:entry> (or relying on the conventional file name) instead of embedding the help text in the checkbox tag.

Suggested change
<f:entry title="SSH Verbose Mode" field="sshVerbose">
<f:checkbox title="Enable verbose SSH output for diagnostics"
help="When enabled, SSH commands will include -vvv flag for detailed diagnostic output. This helps troubleshoot SSH connection issues without requiring the GIT_SSH_COMMAND environment variable."/>
<f:entry title="SSH Verbose Mode" field="sshVerbose"
help="When enabled, SSH commands will include -vvv flag for detailed diagnostic output. This helps troubleshoot SSH connection issues without requiring the GIT_SSH_COMMAND environment variable.">
<f:checkbox title="Enable verbose SSH output for diagnostics"/>

Copilot uses AI. Check for mistakes.
</f:entry>
</f:section>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;

/**
* Security test that proves the environment variable approach prevents
Expand All @@ -35,6 +36,7 @@
*
* @author Mark Waite
*/
@WithJenkins
class CliGitAPISecurityTest {

@TempDir
Expand Down Expand Up @@ -286,4 +288,178 @@ private void executeWrapper(Path wrapper, Path keyFile) throws Exception {
// That's fine - we're just checking for injection
}
}

/**
* Test that SSH verbose mode is disabled by default (no -vvv flag)
*/
@Test
@Issue("JENKINS-71461")
void testSshVerboseModeDisabledByDefault() throws Exception {
workspace = new File(tempDir, "test-default");
workspace.mkdirs();

Path keyFile = createMockSSHKey(workspace);
Path knownHosts = Files.createTempFile("known_hosts", "");

try {
// When Jenkins is not available, SSH verbose should default to false
GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars())
.in(workspace)
.using("git")
.getClient();

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

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

This test relies on the global default for sshVerbose without explicitly resetting it. Since other tests in this class toggle the global configuration, it would be more robust to set GitHostKeyVerificationConfiguration.get().setSshVerbose(false) at the start (and/or in @AfterEach). Also, the comment about “When Jenkins is not available” is misleading here because the class is annotated with @WithJenkins.

Copilot uses AI. Check for mistakes.
CliGitAPIImpl git = (CliGitAPIImpl) gitClient;

Path sshWrapper;
if (isWindows()) {
sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts);
} else {
sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts);
}

String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8);

// Verify -vvv flag is NOT present
assertFalse(
wrapperContent.contains("-vvv"),
"Wrapper should NOT contain -vvv flag when verbose mode is disabled");

} finally {
Files.deleteIfExists(knownHosts);
}
}

/**
* Test that SSH verbose mode adds -vvv flag when enabled
*/
@Test
@Issue("JENKINS-71461")
void testSshVerboseModeEnabled() throws Exception {
// Skip if Jenkins instance is not available
if (jenkins.model.Jenkins.getInstanceOrNull() == null) {
return;
}

workspace = new File(tempDir, "test-verbose");
workspace.mkdirs();

Path keyFile = createMockSSHKey(workspace);
Path knownHosts = Files.createTempFile("known_hosts", "");

try {
// Enable SSH verbose mode
GitHostKeyVerificationConfiguration.get().setSshVerbose(true);

GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars())
.in(workspace)
.using("git")
.getClient();
CliGitAPIImpl git = (CliGitAPIImpl) gitClient;

Path sshWrapper;
if (isWindows()) {
sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts);
} else {
sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts);
}

String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8);

// Verify -vvv flag IS present
assertTrue(
wrapperContent.contains("-vvv"), "Wrapper should contain -vvv flag when verbose mode is enabled");

} finally {
// Reset to default
GitHostKeyVerificationConfiguration.get().setSshVerbose(false);
Files.deleteIfExists(knownHosts);
}
}

/**
* Test that SSH verbose mode flag is placed correctly in Unix wrapper
*/
@Test
@Issue("JENKINS-71461")
void testUnixSshVerboseFlagPlacement() throws Exception {
// Skip if Jenkins instance is not available or on Windows
if (jenkins.model.Jenkins.getInstanceOrNull() == null || isWindows()) {
return;
}

workspace = new File(tempDir, "test-unix-verbose");
workspace.mkdirs();

Path keyFile = createMockSSHKey(workspace);
Path knownHosts = Files.createTempFile("known_hosts", "");

try {
// Enable SSH verbose mode
GitHostKeyVerificationConfiguration.get().setSshVerbose(true);

GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars())
.in(workspace)
.using("git")
.getClient();
CliGitAPIImpl git = (CliGitAPIImpl) gitClient;
Path sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts);

String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8);

// Verify -vvv appears before "$@" (which represents additional args)
int vvvIndex = wrapperContent.indexOf("-vvv");
int argsIndex = wrapperContent.indexOf("\"$@\"");
assertTrue(vvvIndex > 0, "-vvv flag should be present");
assertTrue(argsIndex > 0, "\"$@\" should be present");
assertTrue(vvvIndex < argsIndex, "-vvv flag should appear before \"$@\"");

} finally {
// Reset to default
GitHostKeyVerificationConfiguration.get().setSshVerbose(false);
Files.deleteIfExists(knownHosts);
}
}

/**
* Test that SSH verbose mode flag is placed correctly in Windows wrapper
*/
@Test
@Issue("JENKINS-71461")
void testWindowsSshVerboseFlagPlacement() throws Exception {
// Skip if Jenkins instance is not available or on Unix
if (jenkins.model.Jenkins.getInstanceOrNull() == null || !isWindows()) {
return; // Skip on Unix
}

workspace = new File(tempDir, "test-windows-verbose");
workspace.mkdirs();

Path keyFile = createMockSSHKey(workspace);
Path knownHosts = Files.createTempFile("known_hosts", "");

try {
// Enable SSH verbose mode
GitHostKeyVerificationConfiguration.get().setSshVerbose(true);

GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars())
.in(workspace)
.using("git")
.getClient();
CliGitAPIImpl git = (CliGitAPIImpl) gitClient;
Path sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts);

String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8);

// Verify -vvv appears before %* (which represents additional args)
int vvvIndex = wrapperContent.indexOf("-vvv");
int argsIndex = wrapperContent.indexOf("%*");
assertTrue(vvvIndex > 0, "-vvv flag should be present");
assertTrue(argsIndex > 0, "%* should be present");
assertTrue(vvvIndex < argsIndex, "-vvv flag should appear before %*");

} finally {
// Reset to default
GitHostKeyVerificationConfiguration.get().setSshVerbose(false);
Files.deleteIfExists(knownHosts);
}
}
}
Loading