Skip to content

Commit 534f05f

Browse files
committed
Add Content Security Policy and HTTP response size telemetry
1 parent a9dbba6 commit 534f05f

File tree

8 files changed

+266
-3
lines changed

8 files changed

+266
-3
lines changed

dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/RumHttpServletResponseWrapper.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,40 @@ public PrintWriter getWriter() throws IOException {
6969
return printWriter;
7070
}
7171

72+
@Override
73+
public void setHeader(String name, String value) {
74+
if (name != null) {
75+
String lowerName = name.toLowerCase();
76+
if (lowerName.startsWith("content-security-policy")) {
77+
RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected();
78+
} else if (lowerName.equals("content-length") && value != null) {
79+
try {
80+
long contentLength = Long.parseLong(value);
81+
RumInjector.getTelemetryCollector().onInjectionResponseSize(contentLength);
82+
} catch (NumberFormatException ignored) {
83+
// ignore?
84+
}
85+
}
86+
}
87+
super.setHeader(name, value);
88+
}
89+
90+
@Override
91+
public void addHeader(String name, String value) {
92+
if (name != null) {
93+
String lowerName = name.toLowerCase();
94+
if (lowerName.startsWith("content-security-policy")) {
95+
RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected();
96+
}
97+
}
98+
super.addHeader(name, value);
99+
}
100+
72101
@Override
73102
public void setContentLength(int len) {
103+
if (len >= 0) {
104+
RumInjector.getTelemetryCollector().onInjectionResponseSize(len);
105+
}
74106
// don't set it since we don't know if we will inject
75107
}
76108

dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/RumHttpServletResponseWrapperTest.groovy

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,57 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner {
8282
then:
8383
1 * mockTelemetryCollector.onInjectionFailed()
8484
}
85+
86+
void 'setHeader with Content-Security-Policy reports CSP detected'() {
87+
when:
88+
wrapper.setHeader("Content-Security-Policy", "test")
89+
90+
then:
91+
1 * mockTelemetryCollector.onContentSecurityPolicyDetected()
92+
1 * mockResponse.setHeader("Content-Security-Policy", "test")
93+
}
94+
95+
void 'addHeader with Content-Security-Policy-Report-Only reports CSP detected'() {
96+
when:
97+
wrapper.addHeader("Content-Security-Policy-Report-Only", "test")
98+
99+
then:
100+
1 * mockTelemetryCollector.onContentSecurityPolicyDetected()
101+
1 * mockResponse.addHeader("Content-Security-Policy-Report-Only", "test")
102+
}
103+
104+
void 'setHeader with non-CSP header does not report CSP detected'() {
105+
when:
106+
wrapper.setHeader("X-Content-Security-Policy", "test")
107+
108+
then:
109+
0 * mockTelemetryCollector.onContentSecurityPolicyDetected()
110+
1 * mockResponse.setHeader("X-Content-Security-Policy", "test")
111+
}
112+
113+
void 'addHeader with non-CSP header does not report CSP detected'() {
114+
when:
115+
wrapper.addHeader("X-Content-Security-Policy", "test")
116+
117+
then:
118+
0 * mockTelemetryCollector.onContentSecurityPolicyDetected()
119+
1 * mockResponse.addHeader("X-Content-Security-Policy", "test")
120+
}
121+
122+
void 'setHeader with Content-Length reports response size'() {
123+
when:
124+
wrapper.setHeader("Content-Length", "1024")
125+
126+
then:
127+
1 * mockTelemetryCollector.onInjectionResponseSize(1024)
128+
1 * mockResponse.setHeader("Content-Length", "1024")
129+
}
130+
131+
void 'setContentLength method reports response size'() {
132+
when:
133+
wrapper.setContentLength(1024)
134+
135+
then:
136+
1 * mockTelemetryCollector.onInjectionResponseSize(1024)
137+
}
85138
}

dd-java-agent/instrumentation/servlet/request-5/src/main/java/datadog/trace/instrumentation/servlet5/RumHttpServletResponseWrapper.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,40 @@ public PrintWriter getWriter() throws IOException {
6969
return printWriter;
7070
}
7171

72+
@Override
73+
public void setHeader(String name, String value) {
74+
if (name != null) {
75+
String lowerName = name.toLowerCase();
76+
if (lowerName.startsWith("content-security-policy")) {
77+
RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected();
78+
} else if (lowerName.equals("content-length") && value != null) {
79+
try {
80+
long contentLength = Long.parseLong(value);
81+
RumInjector.getTelemetryCollector().onInjectionResponseSize(contentLength);
82+
} catch (NumberFormatException ignored) {
83+
// ignore?
84+
}
85+
}
86+
}
87+
super.setHeader(name, value);
88+
}
89+
90+
@Override
91+
public void addHeader(String name, String value) {
92+
if (name != null) {
93+
String lowerName = name.toLowerCase();
94+
if (lowerName.startsWith("content-security-policy")) {
95+
RumInjector.getTelemetryCollector().onContentSecurityPolicyDetected();
96+
}
97+
}
98+
super.addHeader(name, value);
99+
}
100+
72101
@Override
73102
public void setContentLength(int len) {
103+
if (len >= 0) {
104+
RumInjector.getTelemetryCollector().onInjectionResponseSize(len);
105+
}
74106
// don't set it since we don't know if we will inject
75107
}
76108

dd-java-agent/instrumentation/servlet/request-5/src/test/groovy/RumHttpServletResponseWrapperTest.groovy

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,57 @@ class RumHttpServletResponseWrapperTest extends AgentTestRunner {
8282
then:
8383
1 * mockTelemetryCollector.onInjectionFailed()
8484
}
85+
86+
void 'setHeader with Content-Security-Policy reports CSP detected'() {
87+
when:
88+
wrapper.setHeader("Content-Security-Policy", "test")
89+
90+
then:
91+
1 * mockTelemetryCollector.onContentSecurityPolicyDetected()
92+
1 * mockResponse.setHeader("Content-Security-Policy", "test")
93+
}
94+
95+
void 'addHeader with Content-Security-Policy-Report-Only reports CSP detected'() {
96+
when:
97+
wrapper.addHeader("Content-Security-Policy-Report-Only", "test")
98+
99+
then:
100+
1 * mockTelemetryCollector.onContentSecurityPolicyDetected()
101+
1 * mockResponse.addHeader("Content-Security-Policy-Report-Only", "test")
102+
}
103+
104+
void 'setHeader with non-CSP header does not report CSP detected'() {
105+
when:
106+
wrapper.setHeader("X-Content-Security-Policy", "test")
107+
108+
then:
109+
0 * mockTelemetryCollector.onContentSecurityPolicyDetected()
110+
1 * mockResponse.setHeader("X-Content-Security-Policy", "test")
111+
}
112+
113+
void 'addHeader with non-CSP header does not report CSP detected'() {
114+
when:
115+
wrapper.addHeader("X-Content-Security-Policy", "test")
116+
117+
then:
118+
0 * mockTelemetryCollector.onContentSecurityPolicyDetected()
119+
1 * mockResponse.addHeader("X-Content-Security-Policy", "test")
120+
}
121+
122+
void 'setHeader with Content-Length reports response size'() {
123+
when:
124+
wrapper.setHeader("Content-Length", "1024")
125+
126+
then:
127+
1 * mockTelemetryCollector.onInjectionResponseSize(1024)
128+
1 * mockResponse.setHeader("Content-Length", "1024")
129+
}
130+
131+
void 'setContentLength method reports response size'() {
132+
when:
133+
wrapper.setContentLength(1024)
134+
135+
then:
136+
1 * mockTelemetryCollector.onInjectionResponseSize(1024)
137+
}
85138
}

internal-api/src/main/java/datadog/trace/api/rum/RumInjectorMetrics.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class RumInjectorMetrics implements RumTelemetryCollector {
2323
private final AtomicLong injectionSucceed = new AtomicLong();
2424
private final AtomicLong injectionFailed = new AtomicLong();
2525
private final AtomicLong injectionSkipped = new AtomicLong();
26+
private final AtomicLong contentSecurityPolicyDetected = new AtomicLong();
2627

2728
private final StatsDClient statsd;
2829
private final long interval;
@@ -61,6 +62,17 @@ public void onInjectionSkipped() {
6162
injectionSkipped.incrementAndGet();
6263
}
6364

65+
@Override
66+
public void onContentSecurityPolicyDetected() {
67+
contentSecurityPolicyDetected.incrementAndGet();
68+
}
69+
70+
@Override
71+
public void onInjectionResponseSize(long bytes) {
72+
// report distribution metric immediately
73+
statsd.distribution("rum.injection.response.bytes", bytes, NO_TAGS);
74+
}
75+
6476
public void close() {
6577
if (null != cancellation) {
6678
cancellation.cancel();
@@ -73,12 +85,14 @@ public String summary() {
7385
+ "\ninjectionFailed="
7486
+ injectionFailed.get()
7587
+ "\ninjectionSkipped="
76-
+ injectionSkipped.get();
88+
+ injectionSkipped.get()
89+
+ "\ncontentSecurityPolicyDetected="
90+
+ contentSecurityPolicyDetected.get();
7791
}
7892

7993
private static class Flush implements AgentTaskScheduler.Task<RumInjectorMetrics> {
8094

81-
private final long[] previousCounts = new long[3]; // one per counter
95+
private final long[] previousCounts = new long[4]; // one per counter
8296
private int countIndex;
8397

8498
@Override
@@ -88,6 +102,11 @@ public void run(RumInjectorMetrics target) {
88102
reportIfChanged(target.statsd, "rum.injection.succeed", target.injectionSucceed, NO_TAGS);
89103
reportIfChanged(target.statsd, "rum.injection.failed", target.injectionFailed, NO_TAGS);
90104
reportIfChanged(target.statsd, "rum.injection.skipped", target.injectionSkipped, NO_TAGS);
105+
reportIfChanged(
106+
target.statsd,
107+
"rum.injection.content_security_policy",
108+
target.contentSecurityPolicyDetected,
109+
NO_TAGS);
91110
} catch (ArrayIndexOutOfBoundsException e) {
92111
log.warn(
93112
"previousCounts array needs resizing to at least {}, was {}",

internal-api/src/main/java/datadog/trace/api/rum/RumTelemetryCollector.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ public void onInjectionFailed() {}
1818
@Override
1919
public void onInjectionSkipped() {}
2020

21+
@Override
22+
public void onContentSecurityPolicyDetected() {}
23+
24+
@Override
25+
public void onInjectionResponseSize(long bytes) {}
26+
2127
@Override
2228
public void close() {}
2329

@@ -38,6 +44,12 @@ default void start() {}
3844
// call when RUM injection is skipped
3945
void onInjectionSkipped();
4046

47+
// call when a Content Security Policy header is detected
48+
void onContentSecurityPolicyDetected();
49+
50+
// call to get the response size (in bytes) before RUM injection
51+
void onInjectionResponseSize(long bytes);
52+
4153
default void close() {}
4254

4355
// human-readable summary of the current health metrics

internal-api/src/test/groovy/datadog/trace/api/rum/RumInjectorMetricsTest.groovy

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,22 +67,55 @@ class RumInjectorMetricsTest extends Specification {
6767
metrics.close()
6868
}
6969

70+
def "test onContentSecurityPolicyDetected"() {
71+
setup:
72+
def latch = new CountDownLatch(1)
73+
def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS)
74+
metrics.start()
75+
76+
when:
77+
metrics.onContentSecurityPolicyDetected()
78+
latch.await(5, TimeUnit.SECONDS)
79+
80+
then:
81+
1 * statsD.count('rum.injection.content_security_policy', 1, _)
82+
0 * _
83+
84+
cleanup:
85+
metrics.close()
86+
}
87+
88+
def "test onInjectionResponseSize with multiple sizes"() {
89+
when:
90+
metrics.onInjectionResponseSize(512)
91+
metrics.onInjectionResponseSize(2048)
92+
metrics.onInjectionResponseSize(256)
93+
94+
then:
95+
1 * statsD.distribution('rum.injection.response.bytes', 512, _)
96+
1 * statsD.distribution('rum.injection.response.bytes', 2048, _)
97+
1 * statsD.distribution('rum.injection.response.bytes', 256, _)
98+
0 * _
99+
}
100+
70101
def "test flushing multiple events"() {
71102
setup:
72-
def latch = new CountDownLatch(3) // expecting 3 metric types
103+
def latch = new CountDownLatch(4) // expecting 4 metric types
73104
def metrics = new RumInjectorMetrics(new Latched(statsD, latch), 10, TimeUnit.MILLISECONDS)
74105
metrics.start()
75106

76107
when:
77108
metrics.onInjectionSucceed()
78109
metrics.onInjectionFailed()
79110
metrics.onInjectionSkipped()
111+
metrics.onContentSecurityPolicyDetected()
80112
latch.await(5, TimeUnit.SECONDS)
81113

82114
then:
83115
1 * statsD.count('rum.injection.succeed', 1, _)
84116
1 * statsD.count('rum.injection.failed', 1, _)
85117
1 * statsD.count('rum.injection.skipped', 1, _)
118+
1 * statsD.count('rum.injection.content_security_policy', 1, _)
86119
0 * _
87120

88121
cleanup:
@@ -105,6 +138,7 @@ class RumInjectorMetricsTest extends Specification {
105138
// should not be called since they have delta of 0
106139
0 * statsD.count('rum.injection.failed', _, _)
107140
0 * statsD.count('rum.injection.skipped', _, _)
141+
0 * statsD.count('rum.injection.content_security_policy', _, _)
108142
0 * _
109143

110144
cleanup:
@@ -119,12 +153,15 @@ class RumInjectorMetricsTest extends Specification {
119153
metrics.onInjectionFailed()
120154
metrics.onInjectionSucceed()
121155
metrics.onInjectionSkipped()
156+
metrics.onContentSecurityPolicyDetected()
157+
metrics.onContentSecurityPolicyDetected()
122158
def summary = metrics.summary()
123159

124160
then:
125161
summary.contains("injectionSucceed=3")
126162
summary.contains("injectionFailed=2")
127163
summary.contains("injectionSkipped=1")
164+
summary.contains("contentSecurityPolicyDetected=2")
128165
0 * _
129166
}
130167

@@ -136,6 +173,7 @@ class RumInjectorMetricsTest extends Specification {
136173
summary.contains("injectionSucceed=0")
137174
summary.contains("injectionFailed=0")
138175
summary.contains("injectionSkipped=0")
176+
summary.contains("contentSecurityPolicyDetected=0")
139177
0 * _
140178
}
141179

0 commit comments

Comments
 (0)