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) {
StandardUsernamePasswordCredentials supc = (StandardUsernamePasswordCredentials)credential;
if (supc.isUsernameSecret()) {
sensitiveStrings.add(supc.getUsername());
}
sensitiveStrings.add(supc.getPassword().getPlainText());
} else if (credential instanceof SSHUserPrivateKey) {
SSHUserPrivateKey sshUserPrivateKey = (SSHUserPrivateKey) credential;
if (sshUserPrivateKey.isUsernameSecret()) {
sensitiveStrings.add(sshUserPrivateKey.getUsername());
}
List<String> privateKeys = sshUserPrivateKey.getPrivateKeys();
if (!sshUserPrivateKey.getPrivateKeys().isEmpty()) {
// only the first key is supported
sensitiveStrings.add(privateKeys.get(0));
}
} else if (credential instanceof StringCredentials) {
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)) {
AbstractProject<?, ?> parent = (AbstractProject<?, ?>) build.getParent();
if (parent.getClass().getSimpleName().equals("MavenModuleSet")) {
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) {
LOGGER.log(Level.WARNING, "Unable to mask secrets for " + parent.getFullName() + "#" + build.getNumber(), e);
PrintStream ps = new PrintStream(logger, false, build.getCharset());
e.printStackTrace(ps);
ps.flush();
assert false : "MavenModuleSet API has changed in an incompatable way";
}
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.