Skip to content

Commit

Permalink
Merge pull request #28 from abayer/jenkins-24805
Browse files Browse the repository at this point in the history
[JENKINS-24805] Start masking secrets in freestyle logs.
  • Loading branch information
jglick committed Oct 31, 2016
2 parents 686a29f + 4548781 commit d2bcf97
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 10 deletions.
Expand Up @@ -38,14 +38,19 @@
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.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;

/**
Expand Down Expand Up @@ -138,4 +143,20 @@ protected static final class NullUnbinder implements Unbinder {
return (BindingDescriptor<C>) super.getDescriptor();
}

/**
* Utility method for turning a collection of secret strings into a single {@link String} for pattern compilation.
* @param secrets A collection of secret strings
* @return A {@link String} generated from that collection.
*/
@Restricted(NoExternalUse.class)
public static String getPatternStringForSecrets(Collection<String> secrets) {
StringBuilder b = new StringBuilder();
for (String secret : secrets) {
if (b.length() > 0) {
b.append('|');
}
b.append(Pattern.quote(secret));
}
return b.toString();
}
}
Expand Up @@ -136,14 +136,7 @@ private static final class Filter extends ConsoleLogFilter implements Serializab
private String charsetName;

Filter(Collection<String> 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 = Secret.fromString(MultiBinding.getPatternStringForSecrets(secrets));
this.charsetName = charsetName;
}

Expand Down
Expand Up @@ -26,38 +26,82 @@

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.OutputStream;
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 org.jenkinsci.plugins.credentialsbinding.MultiBinding;
import org.kohsuke.stapler.DataBoundConstructor;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

@SuppressWarnings({"rawtypes", "unchecked"}) // inherited from BuildWrapper
public class SecretBuildWrapper extends BuildWrapper {

private /*almost final*/ List<? extends MultiBinding<?>> bindings;

private final static Map<AbstractBuild<?, ?>, Collection<String>> secretsForBuild = new WeakHashMap<AbstractBuild<?, ?>, Collection<String>>();

/**
* 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 @CheckForNull Pattern getPatternForBuild(@Nonnull AbstractBuild<?, ?> build) {
if (secretsForBuild.containsKey(build)) {
return Pattern.compile(MultiBinding.getPatternStringForSecrets(secretsForBuild.get(build)));
} else {
return null;
}
}

@DataBoundConstructor public SecretBuildWrapper(List<? extends MultiBinding<?>> bindings) {
this.bindings = bindings == null ? Collections.<MultiBinding<?>>emptyList() : bindings;
}

public List<? extends MultiBinding<?>> getBindings() {
return bindings;
}

@Override
public OutputStream decorateLogger(AbstractBuild build, OutputStream logger) throws IOException, InterruptedException, Run.RunnerAbortedException {
return new Filter(build.getCharset().name()).decorateLogger(build, logger);
}

@Override public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
final List<MultiBinding.MultiEnvironment> m = new ArrayList<MultiBinding.MultiEnvironment>();

Set<String> secrets = new HashSet<String>();

for (MultiBinding binding : bindings) {
m.add(binding.bind(build, build.getWorkspace(), launcher, listener));
MultiBinding.MultiEnvironment e = binding.bind(build, build.getWorkspace(), launcher, listener);
m.add(e);
secrets.addAll(e.getValues().values());
}

if (!secrets.isEmpty()) {
secretsForBuild.put(build, secrets);
}

return new Environment() {
@Override public void buildEnvVars(Map<String,String> env) {
for (MultiBinding.MultiEnvironment e : m) {
Expand All @@ -68,6 +112,7 @@ public List<? extends MultiBinding<?>> getBindings() {
for (MultiBinding.MultiEnvironment e : m) {
e.getUnbinder().unbind(build, build.getWorkspace(), launcher, listener);
}
secretsForBuild.remove(build);
return true;
}
};
Expand All @@ -86,6 +131,42 @@ protected Object readResolve() {
return this;
}

/** Similar to {@code MaskPasswordsOutputStream}. */
private static final class Filter extends ConsoleLogFilter {

private final String charsetName;

Filter(String charsetName) {
this.charsetName = charsetName;
}

@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) {
Expand Down
@@ -0,0 +1,87 @@
/*
* The MIT License
*
* Copyright 2016 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.impl;

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.domains.Domain;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Item;
import hudson.tasks.Shell;
import hudson.util.Secret;
import org.jenkinsci.plugins.credentialsbinding.MultiBinding;
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;

import java.util.ArrayList;
import java.util.Collections;

public class SecretBuildWrapperTest {

@Rule public JenkinsRule r = new JenkinsRule();

@Issue("JENKINS-24805")
@Test public void maskingFreeStyleSecrets() throws Exception {
String firstCredentialsId = "creds_1";
String firstPassword = "p4ss";
StringCredentialsImpl firstCreds = new StringCredentialsImpl(CredentialsScope.GLOBAL, firstCredentialsId, "sample1", Secret.fromString(firstPassword));

CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), firstCreds);

SecretBuildWrapper wrapper = new SecretBuildWrapper(Collections.singletonList(new StringBinding("PASS_1", firstCredentialsId)));

FreeStyleProject f = r.createFreeStyleProject();

f.setConcurrentBuild(true);
f.getBuildersList().add(new Shell("echo $PASS_1"));
f.getBuildWrappersList().add(wrapper);

r.configRoundtrip((Item)f);

FreeStyleBuild b = r.buildAndAssertSuccess(f);
r.assertLogNotContains(firstPassword, b);
r.assertLogContains("echo ****", b);
}

@Issue("JENKINS-24805")
@Test public void emptySecretsList() throws Exception {
SecretBuildWrapper wrapper = new SecretBuildWrapper(new ArrayList<MultiBinding<?>>());

FreeStyleProject f = r.createFreeStyleProject();

f.setConcurrentBuild(true);
f.getBuildersList().add(new Shell("echo PASSES"));
f.getBuildWrappersList().add(wrapper);

r.configRoundtrip((Item)f);

FreeStyleBuild b = r.buildAndAssertSuccess(f);
r.assertLogContains("PASSES", b);
}
}

0 comments on commit d2bcf97

Please sign in to comment.