From a47f4b8529fd216c3c797566aa969191834abd7a Mon Sep 17 00:00:00 2001 From: Andrew Bayer Date: Mon, 31 Oct 2016 11:19:31 -0700 Subject: [PATCH] [JENKINS-24805] First work on masking secrets in freestyle logs. This still needs tests - I just want to make sure the approach is right. --- .../credentialsbinding/MultiBinding.java | 18 ++++ .../credentialsbinding/impl/BindingStep.java | 9 +- .../impl/SecretBuildWrapper.java | 97 ++++++++++++++++++- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/MultiBinding.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/MultiBinding.java index 73d37d05..51c6110b 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/MultiBinding.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/MultiBinding.java @@ -38,12 +38,15 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.Serializable; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; import javax.annotation.Nonnull; +import hudson.util.Secret; import jenkins.model.Jenkins; import org.jenkinsci.plugins.credentialsbinding.impl.CredentialNotFoundException; import org.kohsuke.stapler.DataBoundConstructor; @@ -138,4 +141,19 @@ protected static final class NullUnbinder implements Unbinder { return (BindingDescriptor) super.getDescriptor(); } + /** + * Utility method for turning a collection of secret strings into a {@link Secret}. + * @param secrets A collection of secret strings + * @return A {@link Secret} generated from that collection. + */ + public static Secret getSecretForStrings(Collection secrets) { + StringBuilder b = new StringBuilder(); + for (String secret : secrets) { + if (b.length() > 0) { + b.append('|'); + } + b.append(Pattern.quote(secret)); + } + return Secret.fromString(b.toString()); + } } diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/BindingStep.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/BindingStep.java index ce9d3205..2a991953 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/BindingStep.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/BindingStep.java @@ -136,14 +136,7 @@ private static final class Filter extends ConsoleLogFilter implements Serializab private String charsetName; Filter(Collection secrets, String charsetName) { - StringBuilder b = new StringBuilder(); - for (String secret : secrets) { - if (b.length() > 0) { - b.append('|'); - } - b.append(Pattern.quote(secret)); - } - pattern = Secret.fromString(b.toString()); + pattern = MultiBinding.getSecretForStrings(secrets); this.charsetName = charsetName; } diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/SecretBuildWrapper.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/SecretBuildWrapper.java index 921546ea..058ec0e0 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/SecretBuildWrapper.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/SecretBuildWrapper.java @@ -26,48 +26,95 @@ import hudson.Extension; import hudson.Launcher; +import hudson.console.ConsoleLogFilter; +import hudson.console.LineTransformationOutputStream; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; +import hudson.model.Run; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; import java.io.IOException; +import java.io.ObjectStreamException; +import java.io.OutputStream; +import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import hudson.util.Secret; +import org.apache.commons.codec.Charsets; import org.jenkinsci.plugins.credentialsbinding.MultiBinding; import org.kohsuke.stapler.DataBoundConstructor; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + @SuppressWarnings({"rawtypes", "unchecked"}) // inherited from BuildWrapper public class SecretBuildWrapper extends BuildWrapper { private /*almost final*/ List> bindings; + private static Map, Collection> secretsForBuild = new WeakHashMap, Collection>(); + + /** + * Gets the {@link Pattern} for the secret values for a given build, if that build has secrets defined. If not, return + * null. + * @param build A non-null build. + * @return A compiled {@link Pattern} from the build's secret values, if the build has any. + */ + public static @Nullable Pattern getPatternForBuild(@Nonnull AbstractBuild build) { + if (secretsForBuild.containsKey(build)) { + return Pattern.compile(MultiBinding.getSecretForStrings(secretsForBuild.get(build)).getPlainText()); + } else { + return null; + } + } + @DataBoundConstructor public SecretBuildWrapper(List> bindings) { this.bindings = bindings == null ? Collections.>emptyList() : bindings; } - + public List> getBindings() { return bindings; } - @Override public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException { + @Override + public OutputStream decorateLogger(AbstractBuild build, OutputStream logger) throws IOException, InterruptedException, Run.RunnerAbortedException { + if (!bindings.isEmpty()) { + return new Filter(build.getCharset().name()).decorateLogger(build, logger); + } else { + return logger; + } + } + + @Override public Environment setUp(final AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException { final List m = new ArrayList(); for (MultiBinding binding : bindings) { m.add(binding.bind(build, build.getWorkspace(), launcher, listener)); } + + secretsForBuild.put(build, new HashSet()); + return new Environment() { @Override public void buildEnvVars(Map env) { for (MultiBinding.MultiEnvironment e : m) { env.putAll(e.getValues()); + secretsForBuild.get(build).addAll(e.getValues().values()); } } @Override public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException { for (MultiBinding.MultiEnvironment e : m) { e.getUnbinder().unbind(build, build.getWorkspace(), launcher, listener); } + secretsForBuild.remove(build); return true; } }; @@ -86,6 +133,52 @@ protected Object readResolve() { return this; } + /** Similar to {@code MaskPasswordsOutputStream}. */ + private static final class Filter extends ConsoleLogFilter implements Serializable { + + private static final long serialVersionUID = 1; + + private String charsetName; + + Filter(String charsetName) { + this.charsetName = charsetName; + } + + // To avoid de-serialization issues with newly added field (charsetName) + private Object readResolve() throws ObjectStreamException { + if (this.charsetName == null) { + this.charsetName = Charsets.UTF_8.name(); + } + return this; + } + + @Override public OutputStream decorateLogger(final AbstractBuild build, final OutputStream logger) throws IOException, InterruptedException { + return new LineTransformationOutputStream() { + Pattern p; + + @Override protected void eol(byte[] b, int len) throws IOException { + if (p == null) { + p = getPatternForBuild(build); + } + + if (p != null) { + Matcher m = p.matcher(new String(b, 0, len, charsetName)); + if (m.find()) { + logger.write(m.replaceAll("****").getBytes(charsetName)); + } else { + // Avoid byte → char → byte conversion unless we are actually doing something. + logger.write(b, 0, len); + } + } else { + // Avoid byte → char → byte conversion unless we are actually doing something. + logger.write(b, 0, len); + } + } + }; + } + + } + @Extension public static class DescriptorImpl extends BuildWrapperDescriptor { @Override public boolean isApplicable(AbstractProject item) {