diff --git a/pom.xml b/pom.xml index 2491c80b..3839b9fb 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ https://github.com/jenkinsci/${project.artifactId}-plugin 999999-SNAPSHOT - 2.361.4 + 2.387.3 jenkinsci/${project.artifactId}-plugin @@ -51,8 +51,8 @@ io.jenkins.tools.bom - bom-2.361.x - 1887.vda_d0ddb_c15c4 + bom-2.387.x + 2244.vd60654536b_96 import pom 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 5b0644ea..f1f90f25 100644 --- a/src/main/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatterns.java +++ b/src/main/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatterns.java @@ -27,7 +27,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.console.LineTransformationOutputStream; -import java.util.Arrays; import jenkins.util.JenkinsJVM; import java.io.IOException; @@ -36,16 +35,12 @@ import java.util.Comparator; import java.util.List; import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; public class SecretPatterns { - private static final Logger LOGGER = Logger.getLogger(SecretPatterns.class.getName()); - private static final Comparator BY_LENGTH_DESCENDING = Comparator.comparingInt(String::length).reversed().thenComparing(String::compareTo); @@ -59,7 +54,8 @@ public class SecretPatterns { * absence of quoting, the longer form is masked. */ public static @NonNull Pattern getAggregateSecretPattern(@NonNull Collection inputs) { - List secretPatternFactories = getSecretPatternFactories(); + JenkinsJVM.checkJenkinsJVM(); + List secretPatternFactories = SecretPatternFactory.all(); String pattern = inputs.stream() .filter(input -> !input.isEmpty()) .flatMap(input -> @@ -72,31 +68,6 @@ public class SecretPatterns { return Pattern.compile(pattern); } - private static List getSecretPatternFactories() { - if (JenkinsJVM.isJenkinsJVM()) { - return SecretPatternFactory.all(); - } else { - // TODO Change this to a hard fail in future, e.g. JenkinsJVM.checkJenkinsJVM(); - LOGGER.log( - Level.WARNING, - "An agent attempted to look up secret patterns from the controller, which is unsupported. " + - "Falling back to basic implementation that may not mask common transformations of the secret. " + - "This workaround will be removed in a future release. " + - "This is a bug in the plugin calling SecretPatterns#getAggregateSecretPattern(String) " + - "and should be reported to its maintainers. " + - "The plugin can be identified through the stacktrace below.", - new RuntimeException() - ); - return Arrays.asList( - new AlmquistShellSecretPatternFactory(), - new BashSecretPatternFactory(), - new BatchSecretPatternFactory(), - new DollarSecretPatternFactory(), - new LiteralSecretPatternFactory() - ); - } - } - /** * Delegating output stream that masks occurrences of a set of secrets. */ diff --git a/src/test/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatternsTest.java b/src/test/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatternsTest.java new file mode 100644 index 00000000..007e4680 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/credentialsbinding/masking/SecretPatternsTest.java @@ -0,0 +1,83 @@ +/* + * The MIT License + * + * Copyright 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.credentialsbinding.masking; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Set; +import java.util.regex.Pattern; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.log.TaskListenerDecorator; +import org.jenkinsci.plugins.workflow.steps.durable_task.DurableTaskStep; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.FlagRule; +import org.jvnet.hudson.test.InboundAgentRule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; + +public final class SecretPatternsTest { + + @ClassRule public static BuildWatcher watcher = new BuildWatcher(); + @Rule public JenkinsRule r = new JenkinsRule(); + @Rule public InboundAgentRule agents = new InboundAgentRule(); + @Rule public FlagRule useWatching = new FlagRule<>(() -> DurableTaskStep.USE_WATCHING, v -> DurableTaskStep.USE_WATCHING = v); + + @Issue("SECURITY-3075") + @Test public void secretPatternFactoriesRetrievedFromAgent() throws Exception { + DurableTaskStep.USE_WATCHING = true; + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("node('remote') {def msg = 'echo do not look at s3cr3t please'; if (isUnix()) {sh msg} else {bat msg}}", true)); + agents.createAgent(r, "remote"); + try { + WorkflowRun b = r.waitForCompletion(p.scheduleBuild2(0).waitForStart()); + r.assertLogContains(BadMasker.class.getName(), b); + /* Not currently ensured: + r.assertLogNotContains("s3cr3t", b); + */ + } finally { + agents.stop("remote"); + } + } + + public static final class BadMasker extends TaskListenerDecorator { + @Override public OutputStream decorate(OutputStream logger) throws IOException, InterruptedException { + Pattern pattern = SecretPatterns.getAggregateSecretPattern(Set.of("s3cr3t")); + return new SecretPatterns.MaskingOutputStream(logger, () -> pattern, "UTF-8"); + } + @TestExtension("secretPatternFactoriesRetrievedFromAgent") public static final class Factory implements TaskListenerDecorator.Factory { + @Override public TaskListenerDecorator of(FlowExecutionOwner owner) { + return new BadMasker(); + } + } + } + +}