Skip to content

Commit 88fdfa9

Browse files
authored
Add telemetry for Content Security Policy (jenkinsci#23901)
* Add telemetry for Content Security Policy * Add license header * Add header length --------- Co-authored-by: Daniel Beck <daniel-beck@users.noreply.github.com>
1 parent 3b501e7 commit 88fdfa9

File tree

3 files changed

+193
-0
lines changed

3 files changed

+193
-0
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2025, CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package jenkins.telemetry.impl;
26+
27+
import edu.umd.cs.findbugs.annotations.NonNull;
28+
import hudson.Extension;
29+
import hudson.ExtensionList;
30+
import java.time.LocalDate;
31+
import java.util.Map;
32+
import java.util.Optional;
33+
import java.util.Set;
34+
import java.util.TreeSet;
35+
import java.util.logging.Level;
36+
import java.util.logging.Logger;
37+
import java.util.stream.Collectors;
38+
import jenkins.security.csp.AdvancedConfigurationDescriptor;
39+
import jenkins.security.csp.Contributor;
40+
import jenkins.security.csp.CspBuilder;
41+
import jenkins.security.csp.CspHeader;
42+
import jenkins.security.csp.CspHeaderDecider;
43+
import jenkins.security.csp.impl.CspConfiguration;
44+
import jenkins.telemetry.Telemetry;
45+
import net.sf.json.JSONObject;
46+
import org.kohsuke.accmod.Restricted;
47+
import org.kohsuke.accmod.restrictions.NoExternalUse;
48+
49+
/**
50+
* Collect information about Content Security Policy configuration.
51+
*
52+
*/
53+
@Restricted(NoExternalUse.class)
54+
@Extension
55+
public class ContentSecurityPolicy extends Telemetry {
56+
57+
private static final Logger LOGGER = Logger.getLogger(ContentSecurityPolicy.class.getName());
58+
59+
@NonNull
60+
@Override
61+
public String getDisplayName() {
62+
return "Content Security Policy";
63+
}
64+
65+
@NonNull
66+
@Override
67+
public LocalDate getStart() {
68+
return LocalDate.of(2025, 12, 1);
69+
}
70+
71+
@NonNull
72+
@Override
73+
public LocalDate getEnd() {
74+
return LocalDate.of(2026, 6, 1);
75+
}
76+
77+
@Override
78+
public JSONObject createContent() {
79+
final JSONObject data = new JSONObject();
80+
data.put("enforce", ExtensionList.lookupSingleton(CspConfiguration.class).isEnforce());
81+
final Optional<CspHeaderDecider> decider = CspHeaderDecider.getCurrentDecider();
82+
data.put("decider", decider.map(Object::getClass).map(Class::getName).orElse(null));
83+
data.put("header", decider.map(CspHeaderDecider::decide).filter(Optional::isPresent).map(Optional::get).map(CspHeader::getHeaderName).orElse(null));
84+
85+
Set<String> contributors = new TreeSet<>();
86+
ExtensionList.lookup(Contributor.class).stream().map(Contributor::getClass).map(Class::getName).forEach(contributors::add);
87+
data.put("contributors", contributors);
88+
89+
Set<String> configurations = new TreeSet<>();
90+
ExtensionList.lookup(AdvancedConfigurationDescriptor.class).stream().map(AdvancedConfigurationDescriptor::getClass).map(Class::getName).forEach(configurations::add);
91+
data.put("configurations", configurations);
92+
93+
try {
94+
Map<String, Map<String, Integer>> directivesSize = new CspBuilder().withDefaultContributions().getMergedDirectives().stream()
95+
.map(d -> Map.entry(d.name(), Map.of("entries", d.values().size(), "chars", String.join(" ", d.values()).length())))
96+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
97+
data.put("directivesSize", directivesSize);
98+
} catch (RuntimeException ex) {
99+
LOGGER.log(Level.FINE, "Error during directive processing", ex);
100+
}
101+
102+
data.put("components", buildComponentInformation());
103+
return data;
104+
}
105+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core">
3+
This trial collects basic information about the current configuration of Content Security Policy (CSP):
4+
<ul>
5+
<li>Whether the user-facing configuration option is set to enforce CSP</li>
6+
<li>The active <code>CspHeaderDecider</code>, which roughly corresponds to how Jenkins is run and whether system properties related to CSP are set</li>
7+
<li>The current Content Security Policy header (<code>Content-Security-Policy</code> or <code>Content-Security-Policy-Report-Only</code>)</li>
8+
<li>Which <code>Contributor</code>s are known, i.e., extensions that contribute to the CSP rules</li>
9+
<li>Which <code>AdvancedConfiguration</code>s are known, i.e., extensions that add CSP configuration options</li>
10+
<li>Which directives (e.g., <code>img-src</code>) are set, and the length in bytes of their values</li>
11+
</ul>
12+
13+
<p>
14+
<strong>This trial does not collect the actual CSP rules.</strong>
15+
</p>
16+
<p>
17+
Additionally this trial collects the list of installed plugins, their version, and the version of Jenkins.
18+
This data will be used to understand how widely CSP protection is used.
19+
</p>
20+
</j:jelly>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package jenkins.telemetry.impl;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.containsInAnyOrder;
5+
import static org.hamcrest.Matchers.empty;
6+
import static org.hamcrest.Matchers.is;
7+
8+
import hudson.ExtensionList;
9+
import hudson.model.FreeStyleProject;
10+
import jenkins.security.csp.AvatarContributor;
11+
import jenkins.security.csp.Contributor;
12+
import jenkins.security.csp.CspBuilder;
13+
import jenkins.security.csp.CspHeader;
14+
import jenkins.security.csp.Directive;
15+
import jenkins.security.csp.impl.BaseContributor;
16+
import jenkins.security.csp.impl.CompatibleContributor;
17+
import jenkins.security.csp.impl.DevelopmentHeaderDecider;
18+
import jenkins.security.csp.impl.UserAvatarContributor;
19+
import net.sf.json.JSONObject;
20+
import org.junit.jupiter.api.Test;
21+
import org.jvnet.hudson.test.JenkinsRule;
22+
import org.jvnet.hudson.test.TestExtension;
23+
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
24+
25+
@WithJenkins
26+
public class ContentSecurityPolicyTest {
27+
28+
@Test
29+
void basics(JenkinsRule j) { // arg required to actually get a Jenkins
30+
final ContentSecurityPolicy csp = ExtensionList.lookupSingleton(ContentSecurityPolicy.class);
31+
final JSONObject content = csp.createContent();
32+
assertThat(content.get("enforce"), is(false));
33+
assertThat(content.get("decider"), is(DevelopmentHeaderDecider.class.getName()));
34+
assertThat(content.get("header"), is(CspHeader.ContentSecurityPolicy.getHeaderName()));
35+
assertThat(content.getJSONArray("contributors"),
36+
containsInAnyOrder(BaseContributor.class.getName(), CompatibleContributor.class.getName(), AvatarContributor.class.getName(), UserAvatarContributor.class.getName()));
37+
assertThat(content.getJSONArray("configurations"), is(empty()));
38+
final JSONObject directivesSize = content.getJSONObject("directivesSize");
39+
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));
40+
assertThat(directivesSize.getJSONObject(Directive.IMG_SRC).get("entries"), is(2));
41+
assertThat(directivesSize.getJSONObject(Directive.IMG_SRC).get("chars"), is(12)); // 'self' data:
42+
assertThat(directivesSize.getJSONObject(Directive.SCRIPT_SRC).get("entries"), is(2));
43+
assertThat(directivesSize.getJSONObject(Directive.SCRIPT_SRC).get("chars"), is(22)); // 'self' 'report-sample'
44+
}
45+
46+
47+
@Test
48+
void withContributors(JenkinsRule j) throws Exception {
49+
final FreeStyleProject freeStyleProject = j.createFreeStyleProject();
50+
51+
final ContentSecurityPolicy csp = ExtensionList.lookupSingleton(ContentSecurityPolicy.class);
52+
final JSONObject content = csp.createContent();
53+
final JSONObject directivesSize = content.getJSONObject("directivesSize");
54+
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));
55+
assertThat(directivesSize.getJSONObject(Directive.IMG_SRC).get("entries"), is(3));
56+
assertThat(directivesSize.getJSONObject(Directive.IMG_SRC).get("chars"), is(28)); // 'self' data: img.example.com
57+
assertThat(directivesSize.getJSONObject(Directive.SCRIPT_SRC).get("entries"), is(2));
58+
assertThat(directivesSize.getJSONObject(Directive.SCRIPT_SRC).get("chars"), is(22)); // 'self' 'report-sample'
59+
}
60+
61+
@TestExtension("withContributors")
62+
public static class TestContributor implements Contributor {
63+
@Override
64+
public void apply(CspBuilder cspBuilder) {
65+
cspBuilder.add(Directive.IMG_SRC, "img.example.com");
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)