diff --git a/core/src/main/java/jenkins/security/csp/CspHeader.java b/core/src/main/java/jenkins/security/csp/CspHeader.java index 657f2ec20cbb..0c388a531686 100644 --- a/core/src/main/java/jenkins/security/csp/CspHeader.java +++ b/core/src/main/java/jenkins/security/csp/CspHeader.java @@ -24,6 +24,7 @@ package jenkins.security.csp; +import edu.umd.cs.findbugs.annotations.CheckForNull; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.Beta; @@ -35,7 +36,8 @@ @Restricted(Beta.class) public enum CspHeader { ContentSecurityPolicy("Content-Security-Policy"), - ContentSecurityPolicyReportOnly("Content-Security-Policy-Report-Only"); + ContentSecurityPolicyReportOnly("Content-Security-Policy-Report-Only"), + None(null); private final String headerName; @@ -43,6 +45,7 @@ public enum CspHeader { this.headerName = headerName; } + @CheckForNull public String getHeaderName() { return headerName; } diff --git a/core/src/main/java/jenkins/security/csp/impl/CspDecorator.java b/core/src/main/java/jenkins/security/csp/impl/CspDecorator.java index 1b4f7522a2da..54434d937508 100644 --- a/core/src/main/java/jenkins/security/csp/impl/CspDecorator.java +++ b/core/src/main/java/jenkins/security/csp/impl/CspDecorator.java @@ -105,10 +105,11 @@ public String getReportingEndpointsHeaderValue(HttpServletRequest req) { } /** - * Determines the name of the HTTP header to set. + * Determines the name of the HTTP header to set, or {@code null} if none. * * @return the name of the HTTP header to set. */ + @CheckForNull public String getContentSecurityPolicyHeaderName() { final Optional decider = CspHeaderDecider.getCurrentDecider(); if (decider.isPresent()) { diff --git a/core/src/main/java/jenkins/security/csp/impl/CspFilter.java b/core/src/main/java/jenkins/security/csp/impl/CspFilter.java index 4a832be2985f..23ba66a2b667 100644 --- a/core/src/main/java/jenkins/security/csp/impl/CspFilter.java +++ b/core/src/main/java/jenkins/security/csp/impl/CspFilter.java @@ -61,12 +61,14 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha CspDecorator cspDecorator = ExtensionList.lookupSingleton(CspDecorator.class); final String headerName = cspDecorator.getContentSecurityPolicyHeaderName(); + final boolean headerShouldBeSet = headerName != null; // This is the preliminary value outside Stapler request handling (and providing a context object) final String headerValue = cspDecorator.getContentSecurityPolicyHeaderValue(req); final boolean isResourceRequest = ResourceDomainConfiguration.isResourceRequest(req); - if (!isResourceRequest) { + + if (headerShouldBeSet && !isResourceRequest) { // The Filter/Decorator approach needs us to "set" headers rather than "add", so no additional endpoints are supported at the moment. final String reportingEndpoints = cspDecorator.getReportingEndpointsHeaderValue(req); if (reportingEndpoints != null) { @@ -78,14 +80,16 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha try { chain.doFilter(req, rsp); } finally { - try { - final String actualHeader = rsp.getHeader(headerName); - if (!isResourceRequest && hasUnexpectedDifference(headerValue, actualHeader)) { - LOGGER.log(Level.FINE, "CSP header has unexpected differences: Expected '" + headerValue + "' but got '" + actualHeader + "'"); + if (headerShouldBeSet) { + try { + final String actualHeader = rsp.getHeader(headerName); + if (!isResourceRequest && hasUnexpectedDifference(headerValue, actualHeader)) { + LOGGER.log(Level.FINE, "CSP header has unexpected differences: Expected '" + headerValue + "' but got '" + actualHeader + "'"); + } + } catch (RuntimeException e) { + // Be defensive just in case + LOGGER.log(Level.FINER, "Error checking CSP header after request processing", e); } - } catch (RuntimeException e) { - // Be defensive just in case - LOGGER.log(Level.FINER, "Error checking CSP header after request processing", e); } } } diff --git a/core/src/main/java/jenkins/security/csp/impl/SystemPropertyHeaderDecider.java b/core/src/main/java/jenkins/security/csp/impl/SystemPropertyHeaderDecider.java index 02eb8994fdcd..86a55b97c332 100644 --- a/core/src/main/java/jenkins/security/csp/impl/SystemPropertyHeaderDecider.java +++ b/core/src/main/java/jenkins/security/csp/impl/SystemPropertyHeaderDecider.java @@ -25,7 +25,9 @@ package jenkins.security.csp.impl; import hudson.Extension; +import hudson.Util; import java.util.Arrays; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -50,7 +52,8 @@ public Optional decide() { final String systemProperty = SystemProperties.getString(SYSTEM_PROPERTY_NAME); if (systemProperty != null) { LOGGER.log(Level.FINEST, "Using system property: {0}", new Object[]{ systemProperty }); - return Arrays.stream(CspHeader.values()).filter(h -> h.getHeaderName().equals(systemProperty)).findFirst(); + final String expected = Util.fixEmptyAndTrim(systemProperty); + return Arrays.stream(CspHeader.values()).filter(h -> Objects.equals(expected, h.getHeaderName())).findFirst(); } return Optional.empty(); } diff --git a/core/src/main/java/jenkins/telemetry/impl/ContentSecurityPolicy.java b/core/src/main/java/jenkins/telemetry/impl/ContentSecurityPolicy.java new file mode 100644 index 000000000000..d462a1910816 --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/ContentSecurityPolicy.java @@ -0,0 +1,105 @@ +/* + * The MIT License + * + * Copyright (c) 2025, 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 jenkins.telemetry.impl; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.ExtensionList; +import java.time.LocalDate; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import jenkins.security.csp.AdvancedConfigurationDescriptor; +import jenkins.security.csp.Contributor; +import jenkins.security.csp.CspBuilder; +import jenkins.security.csp.CspHeader; +import jenkins.security.csp.CspHeaderDecider; +import jenkins.security.csp.impl.CspConfiguration; +import jenkins.telemetry.Telemetry; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Collect information about Content Security Policy configuration. + * + */ +@Restricted(NoExternalUse.class) +@Extension +public class ContentSecurityPolicy extends Telemetry { + + private static final Logger LOGGER = Logger.getLogger(ContentSecurityPolicy.class.getName()); + + @NonNull + @Override + public String getDisplayName() { + return "Content Security Policy"; + } + + @NonNull + @Override + public LocalDate getStart() { + return LocalDate.of(2025, 12, 1); + } + + @NonNull + @Override + public LocalDate getEnd() { + return LocalDate.of(2026, 6, 1); + } + + @Override + public JSONObject createContent() { + final JSONObject data = new JSONObject(); + data.put("enforce", ExtensionList.lookupSingleton(CspConfiguration.class).isEnforce()); + final Optional decider = CspHeaderDecider.getCurrentDecider(); + data.put("decider", decider.map(Object::getClass).map(Class::getName).orElse(null)); + data.put("header", decider.map(CspHeaderDecider::decide).filter(Optional::isPresent).map(Optional::get).map(CspHeader::getHeaderName).orElse(null)); + + Set contributors = new TreeSet<>(); + ExtensionList.lookup(Contributor.class).stream().map(Contributor::getClass).map(Class::getName).forEach(contributors::add); + data.put("contributors", contributors); + + Set configurations = new TreeSet<>(); + ExtensionList.lookup(AdvancedConfigurationDescriptor.class).stream().map(AdvancedConfigurationDescriptor::getClass).map(Class::getName).forEach(configurations::add); + data.put("configurations", configurations); + + try { + Map> directivesSize = new CspBuilder().withDefaultContributions().getMergedDirectives().stream() + .map(d -> Map.entry(d.name(), Map.of("entries", d.values().size(), "chars", String.join(" ", d.values()).length()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + data.put("directivesSize", directivesSize); + } catch (RuntimeException ex) { + LOGGER.log(Level.FINE, "Error during directive processing", ex); + } + + data.put("components", buildComponentInformation()); + return data; + } +} diff --git a/core/src/main/java/jenkins/telemetry/impl/JavaSystemProperties.java b/core/src/main/java/jenkins/telemetry/impl/JavaSystemProperties.java index 90b66fd6f5bd..210934b9c604 100644 --- a/core/src/main/java/jenkins/telemetry/impl/JavaSystemProperties.java +++ b/core/src/main/java/jenkins/telemetry/impl/JavaSystemProperties.java @@ -70,13 +70,13 @@ public String getDisplayName() { @NonNull @Override public LocalDate getStart() { - return LocalDate.of(2023, 12, 17); + return LocalDate.of(2026, 1, 4); } @NonNull @Override public LocalDate getEnd() { - return LocalDate.of(2024, 4, 1); + return LocalDate.of(2026, 4, 1); } @Override diff --git a/core/src/main/resources/hudson/model/Run/console-log.jelly b/core/src/main/resources/hudson/model/Run/console-log.jelly index bb2aad579a92..42d918818670 100644 --- a/core/src/main/resources/hudson/model/Run/console-log.jelly +++ b/core/src/main/resources/hudson/model/Run/console-log.jelly @@ -29,7 +29,7 @@ -
+      
         
         ${it.writeLogTo(offset,output)}
       
diff --git a/core/src/main/resources/hudson/slaves/Cloud/sidepanel.jelly b/core/src/main/resources/hudson/slaves/Cloud/sidepanel.jelly index 551560f93d3e..b04753c5adc8 100644 --- a/core/src/main/resources/hudson/slaves/Cloud/sidepanel.jelly +++ b/core/src/main/resources/hudson/slaves/Cloud/sidepanel.jelly @@ -27,10 +27,11 @@ THE SOFTWARE. - - + + - + diff --git a/core/src/main/resources/jenkins/security/csp/impl/CspDecorator/httpHeaders.jelly b/core/src/main/resources/jenkins/security/csp/impl/CspDecorator/httpHeaders.jelly index 3595aef7a90c..fed51335066f 100644 --- a/core/src/main/resources/jenkins/security/csp/impl/CspDecorator/httpHeaders.jelly +++ b/core/src/main/resources/jenkins/security/csp/impl/CspDecorator/httpHeaders.jelly @@ -23,6 +23,9 @@ THE SOFTWARE. --> - - + + + + + diff --git a/core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.jelly b/core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.jelly index 617088bc2497..252d558b9e6a 100644 --- a/core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.jelly +++ b/core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.jelly @@ -23,5 +23,14 @@ THE SOFTWARE. --> - ${%blurb(it.headerName)} + + + + ${%blurb(it.headerName)} + + + ${%blurbUnset} + + + diff --git a/core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.properties b/core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.properties index 4af31279ed3a..584e4ed9c6c6 100644 --- a/core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.properties +++ b/core/src/main/resources/jenkins/security/csp/impl/SystemPropertyHeaderDecider/message.properties @@ -1,2 +1,4 @@ blurb = Content Security Policy is configured to use the HTTP header {0} based on the system property jenkins.security.csp.CspHeader.headerName. \ - It cannot be configured through the UI while this system property specifies a header name. + It cannot be configured through the UI while this system property is set. +blurbUnset = Content Security Policy is disabled based on the system property jenkins.security.csp.CspHeader.headerName. \ + It cannot be configured through the UI while this system property is set. diff --git a/core/src/main/resources/jenkins/telemetry/impl/ContentSecurityPolicy/description.jelly b/core/src/main/resources/jenkins/telemetry/impl/ContentSecurityPolicy/description.jelly new file mode 100644 index 000000000000..22ee0e2bd087 --- /dev/null +++ b/core/src/main/resources/jenkins/telemetry/impl/ContentSecurityPolicy/description.jelly @@ -0,0 +1,20 @@ + + + This trial collects basic information about the current configuration of Content Security Policy (CSP): +
    +
  • Whether the user-facing configuration option is set to enforce CSP
  • +
  • The active CspHeaderDecider, which roughly corresponds to how Jenkins is run and whether system properties related to CSP are set
  • +
  • The current Content Security Policy header (Content-Security-Policy or Content-Security-Policy-Report-Only)
  • +
  • Which Contributors are known, i.e., extensions that contribute to the CSP rules
  • +
  • Which AdvancedConfigurations are known, i.e., extensions that add CSP configuration options
  • +
  • Which directives (e.g., img-src) are set, and the length in bytes of their values
  • +
+ +

+ This trial does not collect the actual CSP rules. +

+

+ Additionally this trial collects the list of installed plugins, their version, and the version of Jenkins. + This data will be used to understand how widely CSP protection is used. +

+
diff --git a/pom.xml b/pom.xml index 967cc1d0acd7..38b69e050705 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ THE SOFTWARE. false false - 8.1023.v8b_42b_1b_79b_f7 + 8.1029.vd3071f6b_5988 24.11.1 diff --git a/test/src/test/java/jenkins/security/csp/WinstoneResponseHeaderLengthTest.java b/test/src/test/java/jenkins/security/csp/WinstoneResponseHeaderLengthTest.java new file mode 100644 index 000000000000..09e2c73ebc62 --- /dev/null +++ b/test/src/test/java/jenkins/security/csp/WinstoneResponseHeaderLengthTest.java @@ -0,0 +1,44 @@ +package jenkins.security.csp; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasLength; +import static org.hamcrest.Matchers.is; + +import org.htmlunit.FailingHttpStatusCodeException; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlPage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.jvnet.hudson.test.junit.jupiter.RealJenkinsExtension; + +public class WinstoneResponseHeaderLengthTest { + + @RegisterExtension + public RealJenkinsExtension extension = new RealJenkinsExtension().addSyntheticPlugin(new RealJenkinsExtension.SyntheticPlugin(jenkins.security.csp.winstoneResponseHeaderLengthTest.ContributorImpl.class)); + + @Test + void testLength() throws Exception { + extension.startJenkins(); + String lastHeader = ""; + try (WebClient wc = new WebClient()) { + // Hopefully speed this up a bit: + wc.getOptions().setJavaScriptEnabled(false); + wc.getOptions().setCssEnabled(false); + wc.getOptions().setDownloadImages(false); + wc.getPage(extension.getUrl()); // request once outside try/catch to ensure it works in principle + try { + while (true) { + final HtmlPage htmlPage = wc.getPage(extension.getUrl()); + lastHeader = htmlPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy"); + } + } catch (FailingHttpStatusCodeException e) { + assertThat(e.getStatusCode(), is(500)); + assertThat(e.getResponse().getContentAsString(), containsString("Error 500 Response Header Fields Too Large")); + + assertThat(lastHeader, hasLength(greaterThan(30_000))); + } + } + } +} diff --git a/test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java b/test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java index ef2e6c79f710..bb50b6e30feb 100644 --- a/test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java +++ b/test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java @@ -104,6 +104,31 @@ public void testDefaultWithSystemPropertyUnenforce(JenkinsRule j) throws IOExcep } } + @Test + public void testDefaultWithSystemPropertyNone(JenkinsRule j) throws IOException, SAXException { + System.setProperty(SystemPropertyHeaderDecider.SYSTEM_PROPERTY_NAME, ""); + try (JenkinsRule.WebClient webClient = j.createWebClient()) { + final Optional decider = CspHeaderDecider.getCurrentDecider(); + assertTrue(decider.isPresent()); + assertThat(decider.get(), instanceOf(SystemPropertyHeaderDecider.class)); + + assertFalse(ExtensionList.lookupSingleton(CspRecommendation.class).isActivated()); + + final HtmlPage htmlPage = webClient.goTo("configureSecurity"); + assertThat( + htmlPage.getWebResponse().getContentAsString(), + hasMessage(jellyResource(SystemPropertyHeaderDecider.class, "message.properties"), "blurbUnset")); + assertThat(htmlPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy"), nullValue()); + assertThat(htmlPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy-Report-Only"), nullValue()); + + // submit form and confirm this didn't create a config file + htmlPage.getFormByName("config").submit(htmlPage.getFormByName("config").getButtonByName("Submit")); + assertFalse(ExtensionList.lookupSingleton(CspConfiguration.class).getConfigFile().exists()); + } finally { + System.clearProperty(SystemPropertyHeaderDecider.SYSTEM_PROPERTY_NAME); + } + } + @Test public void testDefaultWithSystemPropertyWrong(JenkinsRule j) throws IOException, SAXException { System.setProperty(SystemPropertyHeaderDecider.SYSTEM_PROPERTY_NAME, "Some-Other-Value"); @@ -219,7 +244,11 @@ public void testFallbackAdminMonitorAndSetup(JenkinsRule j) throws IOException, } private static Matcher hasBlurb(Properties props) { - return containsString(props.getProperty("blurb")); + return hasMessage(props, "blurb"); + } + + private static Matcher hasMessage(Properties props, String key) { + return containsString(props.getProperty(key)); } private static Properties jellyResource(Class clazz, String filename) throws IOException { diff --git a/test/src/test/java/jenkins/security/csp/winstoneResponseHeaderLengthTest/ContributorImpl.java b/test/src/test/java/jenkins/security/csp/winstoneResponseHeaderLengthTest/ContributorImpl.java new file mode 100644 index 000000000000..784be3382786 --- /dev/null +++ b/test/src/test/java/jenkins/security/csp/winstoneResponseHeaderLengthTest/ContributorImpl.java @@ -0,0 +1,28 @@ +package jenkins.security.csp.winstoneResponseHeaderLengthTest; + +import hudson.Extension; +import jenkins.model.Jenkins; +import jenkins.security.csp.Contributor; +import jenkins.security.csp.CspBuilder; +import jenkins.security.csp.Directive; + +public class ContributorImpl implements Contributor { + private int count = 0; + + @Override + public void apply(CspBuilder cspBuilder) { + count++; + for (int i = 0; i < count; i++) { + cspBuilder.add(Directive.IMG_SRC, "img" + i + ".example.com"); + } + } + + @Extension + public static ContributorImpl getInstance() { + // Only load this extension if it's in the synthetic plugin, otherwise it will affect other tests + if (Jenkins.get().getPluginManager().whichPlugin(ContributorImpl.class) == null) { + return null; + } + return new ContributorImpl(); + } +} diff --git a/test/src/test/java/jenkins/telemetry/impl/ContentSecurityPolicyTest.java b/test/src/test/java/jenkins/telemetry/impl/ContentSecurityPolicyTest.java new file mode 100644 index 000000000000..f41171180d79 --- /dev/null +++ b/test/src/test/java/jenkins/telemetry/impl/ContentSecurityPolicyTest.java @@ -0,0 +1,68 @@ +package jenkins.telemetry.impl; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; + +import hudson.ExtensionList; +import hudson.model.FreeStyleProject; +import jenkins.security.csp.AvatarContributor; +import jenkins.security.csp.Contributor; +import jenkins.security.csp.CspBuilder; +import jenkins.security.csp.CspHeader; +import jenkins.security.csp.Directive; +import jenkins.security.csp.impl.BaseContributor; +import jenkins.security.csp.impl.CompatibleContributor; +import jenkins.security.csp.impl.DevelopmentHeaderDecider; +import jenkins.security.csp.impl.UserAvatarContributor; +import net.sf.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +@WithJenkins +public class ContentSecurityPolicyTest { + + @Test + void basics(JenkinsRule j) { // arg required to actually get a Jenkins + final ContentSecurityPolicy csp = ExtensionList.lookupSingleton(ContentSecurityPolicy.class); + final JSONObject content = csp.createContent(); + assertThat(content.get("enforce"), is(false)); + assertThat(content.get("decider"), is(DevelopmentHeaderDecider.class.getName())); + assertThat(content.get("header"), is(CspHeader.ContentSecurityPolicy.getHeaderName())); + assertThat(content.getJSONArray("contributors"), + containsInAnyOrder(BaseContributor.class.getName(), CompatibleContributor.class.getName(), AvatarContributor.class.getName(), UserAvatarContributor.class.getName())); + assertThat(content.getJSONArray("configurations"), is(empty())); + final JSONObject directivesSize = content.getJSONObject("directivesSize"); + assertThat(directivesSize.keySet(), containsInAnyOrder(Directive.DEFAULT_SRC, Directive.SCRIPT_SRC, Directive.STYLE_SRC, Directive.IMG_SRC, Directive.FORM_ACTION, Directive.BASE_URI, Directive.FRAME_ANCESTORS)); + assertThat(directivesSize.getJSONObject(Directive.IMG_SRC).get("entries"), is(2)); + assertThat(directivesSize.getJSONObject(Directive.IMG_SRC).get("chars"), is(12)); // 'self' data: + assertThat(directivesSize.getJSONObject(Directive.SCRIPT_SRC).get("entries"), is(2)); + assertThat(directivesSize.getJSONObject(Directive.SCRIPT_SRC).get("chars"), is(22)); // 'self' 'report-sample' + } + + + @Test + void withContributors(JenkinsRule j) throws Exception { + final FreeStyleProject freeStyleProject = j.createFreeStyleProject(); + + final ContentSecurityPolicy csp = ExtensionList.lookupSingleton(ContentSecurityPolicy.class); + final JSONObject content = csp.createContent(); + final JSONObject directivesSize = content.getJSONObject("directivesSize"); + assertThat(directivesSize.keySet(), containsInAnyOrder(Directive.DEFAULT_SRC, Directive.SCRIPT_SRC, Directive.STYLE_SRC, Directive.IMG_SRC, Directive.FORM_ACTION, Directive.BASE_URI, Directive.FRAME_ANCESTORS)); + assertThat(directivesSize.getJSONObject(Directive.IMG_SRC).get("entries"), is(3)); + assertThat(directivesSize.getJSONObject(Directive.IMG_SRC).get("chars"), is(28)); // 'self' data: img.example.com + assertThat(directivesSize.getJSONObject(Directive.SCRIPT_SRC).get("entries"), is(2)); + assertThat(directivesSize.getJSONObject(Directive.SCRIPT_SRC).get("chars"), is(22)); // 'self' 'report-sample' + } + + @TestExtension("withContributors") + public static class TestContributor implements Contributor { + @Override + public void apply(CspBuilder cspBuilder) { + cspBuilder.add(Directive.IMG_SRC, "img.example.com"); + } + } +} diff --git a/war/pom.xml b/war/pom.xml index f0501f6aa847..5dc484c64c0e 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -661,7 +661,7 @@ THE SOFTWARE. contains a version of Jetty that is older than this, trigger Dependabot in jenkinsci/winstone and release the result before proceeding with the update here. --> - 12.1.4 + 12.1.5