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 3a5da075..16f78107 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/BindingStep.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/BindingStep.java @@ -29,7 +29,6 @@ import hudson.FilePath; import hudson.Launcher; import hudson.console.ConsoleLogFilter; -import hudson.console.LineTransformationOutputStream; import hudson.model.AbstractBuild; import hudson.model.Run; import hudson.model.TaskListener; @@ -49,7 +48,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -218,24 +216,8 @@ private Object readResolve() throws ObjectStreamException { return this; } - @Override public OutputStream decorateLogger(AbstractBuild _ignore, final OutputStream logger) throws IOException, InterruptedException { - final Pattern p = Pattern.compile(pattern.getPlainText()); - return new LineTransformationOutputStream.Delegating(logger) { - @Override protected void eol(byte[] b, int len) throws IOException { - if (!p.toString().isEmpty()) { - Matcher m = p.matcher(new String(b, 0, len, charsetName)); - if (m.find()) { - out.write(m.replaceAll("****").getBytes(charsetName)); - } else { - // Avoid byte → char → byte conversion unless we are actually doing something. - out.write(b, 0, len); - } - } else { - // Avoid byte → char → byte conversion unless we are actually doing something. - out.write(b, 0, len); - } - } - }; + @Override public OutputStream decorateLogger(AbstractBuild _ignore, OutputStream logger) throws IOException, InterruptedException { + return new SecretPatterns.MaskingOutputStream(logger, () -> Pattern.compile(pattern.getPlainText()), 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 68f0ff40..03dd53d9 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/SecretBuildWrapper.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/impl/SecretBuildWrapper.java @@ -27,7 +27,6 @@ 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; @@ -43,7 +42,6 @@ import java.io.IOException; import java.io.OutputStream; import java.util.*; -import java.util.regex.Matcher; import java.util.regex.Pattern; @SuppressWarnings({"rawtypes", "unchecked"}) // inherited from BuildWrapper @@ -141,29 +139,8 @@ private static final class Filter extends ConsoleLogFilter { this.charsetName = charsetName; } - @Override public OutputStream decorateLogger(final AbstractBuild build, final OutputStream logger) throws IOException, InterruptedException { - return new LineTransformationOutputStream.Delegating(logger) { - Pattern p; - - @Override protected void eol(byte[] b, int len) throws IOException { - if (p == null) { - p = getPatternForBuild(build); - } - - if (p != null && !p.toString().isEmpty()) { - Matcher m = p.matcher(new String(b, 0, len, charsetName)); - if (m.find()) { - out.write(m.replaceAll("****").getBytes(charsetName)); - } else { - // Avoid byte → char → byte conversion unless we are actually doing something. - out.write(b, 0, len); - } - } else { - // Avoid byte → char → byte conversion unless we are actually doing something. - out.write(b, 0, len); - } - } - + @Override public OutputStream decorateLogger(AbstractBuild build, OutputStream logger) throws IOException, InterruptedException { + return new SecretPatterns.MaskingOutputStream(logger, () -> getPatternForBuild(build), charsetName) { @Override public void close() throws IOException { super.close(); secretsForBuild.remove(build); diff --git a/src/main/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatterns.java b/src/main/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatterns.java index 632ba9e3..f45a772f 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatterns.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatterns.java @@ -24,16 +24,18 @@ package org.jenkinsci.plugins.credentialsbinding.masking; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; - -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.console.LineTransformationOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.Collection; import java.util.Comparator; +import java.util.function.Supplier; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -@Restricted(NoExternalUse.class) public class SecretPatterns { private static final Comparator BY_LENGTH_DESCENDING = @@ -48,7 +50,7 @@ public class SecretPatterns { * For example, {@code bash -x} will only quote arguments echoed when necessary. To avoid leaking the presence or * absence of quoting, the longer form is masked. */ - public static @Nonnull Pattern getAggregateSecretPattern(@Nonnull Collection inputs) { + public static @NonNull Pattern getAggregateSecretPattern(@NonNull Collection inputs) { String pattern = inputs.stream() .filter(input -> !input.isEmpty()) .flatMap(input -> @@ -60,4 +62,47 @@ public class SecretPatterns { .collect(Collectors.joining("|")); return Pattern.compile(pattern); } + + /** + * Delegating output stream that masks occurrences of a set of secrets. + */ + public static class MaskingOutputStream extends LineTransformationOutputStream.Delegating { + + private final @NonNull Supplier secretPattern; + private final @NonNull String charsetName; + private @CheckForNull Pattern p; + + /** + * @param out the base output stream which will not be sent secrets + * @param secretPattern a lazy computation of either the result of {@link #getAggregateSecretPattern}, or null to just skip masking + * @param charsetName the character set to detect strings + */ + public MaskingOutputStream(@NonNull OutputStream out, @NonNull Supplier secretPattern, @NonNull String charsetName) { + super(out); + this.secretPattern = secretPattern; + this.charsetName = charsetName; + } + + @Override protected void eol(byte[] b, int len) throws IOException { + if (p == null) { + p = secretPattern.get(); + } + if (p == null || p.toString().isEmpty()) { + // Avoid byte → char → byte conversion unless we are actually doing something. + out.write(b, 0, len); + } else { + Matcher m = p.matcher(new String(b, 0, len, charsetName)); + if (m.find()) { + out.write(m.replaceAll("****").getBytes(charsetName)); + } else { + // As above. + out.write(b, 0, len); + } + } + } + + } + + private SecretPatterns() {} + }