Skip to content

Commit

Permalink
SECURITY-3090
Browse files Browse the repository at this point in the history
  • Loading branch information
jtnord authored and julieheard committed Aug 2, 2023
1 parent a544a62 commit 0432a80
Show file tree
Hide file tree
Showing 16 changed files with 646 additions and 96 deletions.
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>ssh-credentials</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials-binding</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/org/jenkinsci/lib/configprovider/ConfigProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ of this software and associated documentation files (the "Software"), to deal
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.jenkinsci.lib.configprovider.model.Config;
Expand All @@ -44,6 +45,7 @@ of this software and associated documentation files (the "Software"), to deal
import hudson.model.TaskListener;
import hudson.util.ReflectionUtils;
import jenkins.model.Jenkins;
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;

/**
* A ConfigProvider represents a configuration file (such as Maven's settings.xml) where the user can choose its actual content among several {@linkplain Config concrete contents} that are
Expand Down Expand Up @@ -201,18 +203,37 @@ public void save(Config config) {
/**
* Provide the given content file.
*
* <strong>Implementation Note:</strong>If this is overridden in a sub class and credentials are injected into
* the content - then the implementation must also override {@link #getSensitiveContentForMasking(Config, Run)} to
* avoid accidental disclosure.
*
* @param configFile the file content to be provided
* @param workDir target workspace directory
* @param listener the listener
* @param tempFiles temp files created by this method, these files will
* be deleted by the caller
* @return file content
* @throws IOException in case an exception occurs when providing the content or other needed files
* @see #getSensitiveContentForMasking(Config, Run)
* @since 2.16
*/
@CheckForNull
public String supplyContent(@NonNull Config configFile, Run<?, ?> build, FilePath workDir, TaskListener listener, @NonNull List<String> tempFiles) throws IOException {
return configFile.content;
}

/**
* Obtain a list of sensitive Strings to mask for the given provider and build.
* For example if a {@link UsernamePasswordCredentials} is being
* injected into a file then the password (and possibly the username) from the resolved credential
* would need to be masked and should be returned here.
*
* @param configFile the file content to provide sensitive strings for.
* @param build the build for which the configFile applies.
* @return List of Strings that need to be masked in the console.
*/
public @NonNull List<String> getSensitiveContentForMasking(Config configFile, Run<?, ?> build) {
return Collections.emptyList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
The MIT License
Copyright (c) 2011, Dominik Bartholdi
Copyright (c) 2023, CloudBees Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -23,28 +24,41 @@ of this software and associated documentation files (the "Software"), to deal
*/
package org.jenkinsci.plugins.configfiles.buildwrapper;

import edu.umd.cs.findbugs.annotations.NonNull;

import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.console.ConsoleLogFilter;
import hudson.model.AbstractProject;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildWrapperDescriptor;
import hudson.util.Secret;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import jenkins.tasks.SimpleBuildWrapper;

import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;

import org.jenkinsci.lib.configprovider.model.Config;
import org.jenkinsci.plugins.configfiles.ConfigFiles;
import org.jenkinsci.plugins.credentialsbinding.masking.SecretPatterns;

public class ConfigFileBuildWrapper extends SimpleBuildWrapper {

private List<ManagedFile> managedFiles = new ArrayList<ManagedFile>();
Expand Down Expand Up @@ -75,6 +89,25 @@ public void setUp(Context context, Run<?, ?> build, FilePath workspace, Launcher
}
}

private synchronized List<String> getSecretValuesToMask(Run<?,?> build) {
List<String> seecretsToMask = new ArrayList<>();
for (ManagedFile managedFile : managedFiles) {
Config config = ConfigFiles.getByIdOrNull(build, managedFile.getFileId());
seecretsToMask.addAll(config.getProvider().getSensitiveContentForMasking(config, build));
}
return seecretsToMask;
}

@Override
public ConsoleLogFilter createLoggerDecorator(@NonNull Run<?, ?> build) {
List<String> secretValues = getSecretValuesToMask(build);
if (secretValues.isEmpty()) {
// no secrets so no filtering
return null;
}
return new SecretFilter(secretValues, build.getCharset());
}

public List<ManagedFile> getManagedFiles() {
return managedFiles;
}
Expand Down Expand Up @@ -115,4 +148,23 @@ public void tearDown(Run<?, ?> build, FilePath workspace, Launcher launcher, Tas

}

private static final class SecretFilter extends ConsoleLogFilter implements Serializable {

private static final long serialVersionUID = 1;

private Secret pattern;
private String charset;

SecretFilter(Collection<String> secrets, Charset cs) {
pattern = Secret.fromString(SecretPatterns.getAggregateSecretPattern(secrets).pattern());
charset = cs.name();
}

@Override
public OutputStream decorateLogger(Run build, OutputStream logger) {
return new SecretPatterns.MaskingOutputStream(logger, () -> Pattern.compile(pattern.getPlainText()), charset);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ of this software and associated documentation files (the "Software"), to deal
package org.jenkinsci.plugins.configfiles.custom;

import com.cloudbees.plugins.credentials.common.IdCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.FilePath;
import hudson.model.Run;
import hudson.model.TaskListener;
Expand Down Expand Up @@ -61,4 +62,10 @@ public String supplyContent(Config configFile, Run<?, ?> build, FilePath workDir
}
return fileContent;
}

@Override
public @NonNull List<String> getSensitiveContentForMasking(Config configFile, Run<?, ?> build) {
HasCustomizedCredentialMappings settings = (HasCustomizedCredentialMappings) configFile;
return CustomConfigCredentialsHelper.secretsForMasking(build, settings.getCustomizedCredentialMappings());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.FilePath;
import hudson.model.Run;
import hudson.model.TaskListener;
Expand Down Expand Up @@ -82,6 +83,34 @@ public static Map<String, IdCredentials> resolveCredentials(Run<?, ?> build, fin
return customizedCredentialsMap;
}

public static @NonNull List<String> secretsForMasking(Run<?, ?> build, final List<CustomizedCredentialMapping> customizedCredentialMappings) {
List<String> sensitiveStrings = new ArrayList<>();
final Map<String, IdCredentials> resolveCredentials = resolveCredentials(build, customizedCredentialMappings, TaskListener.NULL);
for (IdCredentials credential : resolveCredentials.values()) {
// username is not used so no need to mask.
if (credential instanceof StandardUsernamePasswordCredentials) {

Check warning on line 91 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 91 is only partially covered, one branch is missing
StandardUsernamePasswordCredentials supc = (StandardUsernamePasswordCredentials)credential;

Check warning on line 92 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 92 is not covered by tests
if (supc.isUsernameSecret()) {

Check warning on line 93 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 93 is only partially covered, 2 branches are missing
sensitiveStrings.add(supc.getUsername());

Check warning on line 94 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 94 is not covered by tests
}
sensitiveStrings.add(supc.getPassword().getPlainText());

Check warning on line 96 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 96 is not covered by tests
} else if (credential instanceof SSHUserPrivateKey) {

Check warning on line 97 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 97 is only partially covered, one branch is missing
SSHUserPrivateKey sshUserPrivateKey = (SSHUserPrivateKey) credential;

Check warning on line 98 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 98 is not covered by tests
if (sshUserPrivateKey.isUsernameSecret()) {

Check warning on line 99 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 99 is only partially covered, 2 branches are missing
sensitiveStrings.add(sshUserPrivateKey.getUsername());

Check warning on line 100 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 100 is not covered by tests
}
List<String> privateKeys = sshUserPrivateKey.getPrivateKeys();

Check warning on line 102 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 102 is not covered by tests
if (!sshUserPrivateKey.getPrivateKeys().isEmpty()) {

Check warning on line 103 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 103 is only partially covered, 2 branches are missing
// only the first key is supported
sensitiveStrings.add(privateKeys.get(0));

Check warning on line 105 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 105 is not covered by tests
}
} else if (credential instanceof StringCredentials) {

Check warning on line 107 in src/main/java/org/jenkinsci/plugins/configfiles/custom/security/CustomConfigCredentialsHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 107 is only partially covered, one branch is missing
sensitiveStrings.add(((StringCredentials)credential).getSecret().getPlainText());
}
}
return sensitiveStrings;
}

public static String fillAuthentication(Run<?, ?> build, FilePath workDir, TaskListener listener,
String customizedContent, Map<String, IdCredentials> customizedCredentialsMap)
throws MacroEvaluationException, IOException, InterruptedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ of this software and associated documentation files (the "Software"), to deal
import java.util.List;
import java.util.Map;

import edu.umd.cs.findbugs.annotations.NonNull;
import org.jenkinsci.lib.configprovider.AbstractConfigProviderImpl;
import org.jenkinsci.lib.configprovider.model.Config;
import org.jenkinsci.lib.configprovider.model.ContentType;
Expand Down Expand Up @@ -79,4 +80,11 @@ public String supplyContent(Config configFile, Run<?, ?> build, FilePath workDir
}
return fileContent;
}

@Override
public @NonNull List<String> getSensitiveContentForMasking(Config configFile, Run<?, ?> build) {
HasServerCredentialMappings settings = (HasServerCredentialMappings) configFile;
return CredentialsHelper.secretsForMasking(build, settings.getServerCredentialMappings());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
The MIT License
Copyright (c) 2023, CloudBees Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package org.jenkinsci.plugins.configfiles.maven.job;

import hudson.Extension;
import hudson.console.ConsoleLogFilter;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.FreeStyleBuild;
import hudson.model.Run;
import hudson.util.Secret;
import jenkins.util.JenkinsJVM;
import org.apache.commons.beanutils.PropertyUtils;
import org.jenkinsci.plugins.credentialsbinding.masking.SecretPatterns;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
* Extension to mask any sensitive credentials provided by this plugin in maven settings (local or global) for the maven job type.
*/
@Extension
public class MvnConsoleLogFilter extends ConsoleLogFilter {

private static final Logger LOGGER = Logger.getLogger(MvnConsoleLogFilter.class.getName());

@Override
public OutputStream decorateLogger(Run build, OutputStream logger) throws IOException, InterruptedException {
if (build instanceof AbstractBuild && !(build instanceof FreeStyleBuild)) {

Check warning on line 59 in src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 59 is only partially covered, one branch is missing
AbstractProject<?, ?> parent = (AbstractProject<?, ?>) build.getParent();
if (parent.getClass().getSimpleName().equals("MavenModuleSet")) {

Check warning on line 61 in src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 61 is only partially covered, one branch is missing
List<String> secretValues = new ArrayList<>();
try { //Maven
Object settings = PropertyUtils.getProperty(parent, "settings");
if (settings instanceof MvnSettingsProvider) {
MvnSettingsProvider provider = (MvnSettingsProvider) settings;
secretValues.addAll(provider.getSensitiveContentForMasking((AbstractBuild)build));
}
Object globalSettings = PropertyUtils.getProperty(parent, "globalSettings");
if (globalSettings instanceof MvnGlobalSettingsProvider) {
MvnGlobalSettingsProvider provider = (MvnGlobalSettingsProvider) globalSettings;
secretValues.addAll(provider.getSensitiveContentForMasking((AbstractBuild)build));
}
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {

Check warning on line 74 in src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 74 is not covered by tests
LOGGER.log(Level.WARNING, "Unable to mask secrets for " + parent.getFullName() + "#" + build.getNumber(), e);

Check warning on line 75 in src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 75 is not covered by tests
PrintStream ps = new PrintStream(logger, false, build.getCharset());

Check warning on line 76 in src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 76 is not covered by tests
e.printStackTrace(ps);

Check warning on line 77 in src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 77 is not covered by tests
ps.flush();

Check warning on line 78 in src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 78 is not covered by tests
assert false : "MavenModuleSet API has changed in an incompatable way";

Check warning on line 79 in src/main/java/org/jenkinsci/plugins/configfiles/maven/job/MvnConsoleLogFilter.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 79 is not covered by tests
}
if (!secretValues.isEmpty()) {
final Secret pattern = Secret.fromString(SecretPatterns.getAggregateSecretPattern(secretValues).pattern());
return new SecretPatterns.MaskingOutputStream(logger,
() -> Pattern.compile(pattern.getPlainText()),
build.getCharset().name());
}
}
}
return logger;
}
}
Loading

0 comments on commit 0432a80

Please sign in to comment.