1use std::borrow::Cow;
6use std::collections::BTreeMap;
7use std::fmt::{self, Write};
8
9use chrono::{DateTime, Utc};
10use relay_protocol::{Annotated, Array, Empty, FromValue, IntoValue, Object, Value};
11use serde::de::{self, Error, IgnoredAny};
12use serde::{Deserialize, Deserializer, Serialize};
13use url::Url;
14
15use crate::processor::ProcessValue;
16use crate::protocol::{
17 Event, HeaderName, HeaderValue, Headers, LogEntry, PairList, Request, TagEntry, Tags,
18};
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub struct InvalidSecurityError;
22
23impl fmt::Display for InvalidSecurityError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 write!(f, "invalid security report")
26 }
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum CspDirective {
31 BaseUri,
32 ChildSrc,
33 ConnectSrc,
34 DefaultSrc,
35 FencedFrameSrc,
36 FontSrc,
37 FormAction,
38 FrameAncestors,
39 FrameSrc,
40 ImgSrc,
41 ManifestSrc,
42 MediaSrc,
43 ObjectSrc,
44 PluginTypes,
45 PrefetchSrc,
46 Referrer,
47 ScriptSrc,
48 ScriptSrcAttr,
49 ScriptSrcElem,
50 StyleSrc,
51 StyleSrcElem,
52 StyleSrcAttr,
53 UpgradeInsecureRequests,
54 WorkerSrc,
55 Sandbox,
56 NavigateTo,
57 ReportUri,
58 ReportTo,
59 BlockAllMixedContent,
60 RequireSriFor,
61 RequireTrustedTypesFor,
62 TrustedTypes,
63}
64
65relay_common::derive_fromstr_and_display!(CspDirective, InvalidSecurityError, {
66 CspDirective::BaseUri => "base-uri",
67 CspDirective::ChildSrc => "child-src",
68 CspDirective::ConnectSrc => "connect-src",
69 CspDirective::DefaultSrc => "default-src",
70 CspDirective::FencedFrameSrc => "fenced-frame-src",
71 CspDirective::FontSrc => "font-src",
72 CspDirective::FormAction => "form-action",
73 CspDirective::FrameAncestors => "frame-ancestors",
74 CspDirective::FrameSrc => "frame-src",
75 CspDirective::ImgSrc => "img-src",
76 CspDirective::ManifestSrc => "manifest-src",
77 CspDirective::MediaSrc => "media-src",
78 CspDirective::ObjectSrc => "object-src",
79 CspDirective::PluginTypes => "plugin-types",
80 CspDirective::PrefetchSrc => "prefetch-src",
81 CspDirective::Referrer => "referrer",
82 CspDirective::ScriptSrc => "script-src",
83 CspDirective::ScriptSrcAttr => "script-src-attr",
84 CspDirective::ScriptSrcElem => "script-src-elem",
85 CspDirective::StyleSrc => "style-src",
86 CspDirective::StyleSrcElem => "style-src-elem",
87 CspDirective::StyleSrcAttr => "style-src-attr",
88 CspDirective::UpgradeInsecureRequests => "upgrade-insecure-requests",
89 CspDirective::WorkerSrc => "worker-src",
90 CspDirective::Sandbox => "sandbox",
91 CspDirective::NavigateTo => "navigate-to",
92 CspDirective::ReportUri => "report-uri",
93 CspDirective::ReportTo => "report-to",
94 CspDirective::BlockAllMixedContent => "block-all-mixed-content",
95 CspDirective::RequireSriFor => "require-sri-for",
96 CspDirective::RequireTrustedTypesFor => "require-trusted-types-for",
97 CspDirective::TrustedTypes => "trusted-types",
98});
99
100relay_common::impl_str_serde!(CspDirective, "a csp directive");
101
102fn is_local(uri: &str) -> bool {
103 matches!(uri, "" | "self" | "'self'")
104}
105
106fn schema_uses_host(schema: &str) -> bool {
107 matches!(
113 schema,
114 "ftp"
115 | "http"
116 | "gopher"
117 | "nntp"
118 | "telnet"
119 | "imap"
120 | "wais"
121 | "file"
122 | "mms"
123 | "https"
124 | "shttp"
125 | "snews"
126 | "prospero"
127 | "rtsp"
128 | "rtspu"
129 | "rsync"
130 | "svn"
131 | "svn+ssh"
132 | "sftp"
133 | "nfs"
134 | "git"
135 | "git+ssh"
136 | "ws"
137 | "wss"
138 )
139}
140
141fn unsplit_uri(schema: &str, host: &str) -> String {
143 if !host.is_empty() || schema_uses_host(schema) {
144 format!("{schema}://{host}")
145 } else if !schema.is_empty() {
146 format!("{schema}:{host}")
147 } else {
148 String::new()
149 }
150}
151
152fn normalize_uri(value: &str) -> Cow<'_, str> {
153 if is_local(value) {
154 return Cow::Borrowed("'self'");
155 }
156
157 if !value.contains(':') {
162 return Cow::Owned(unsplit_uri(value, ""));
163 }
164
165 let url = match Url::parse(value) {
166 Ok(url) => url,
167 Err(_) => return Cow::Borrowed(value),
168 };
169
170 let normalized = match url.scheme() {
171 "http" | "https" => Cow::Borrowed(url.host_str().unwrap_or_default()),
172 scheme => Cow::Owned(unsplit_uri(scheme, url.host_str().unwrap_or_default())),
173 };
174
175 Cow::Owned(match url.port() {
176 Some(port) => format!("{normalized}:{port}"),
177 None => normalized.into_owned(),
178 })
179}
180
181#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
185struct CspRaw {
186 #[serde(
187 skip_serializing_if = "Option::is_none",
188 alias = "effective-directive",
189 alias = "effectiveDirective"
190 )]
191 effective_directive: Option<String>,
192 #[serde(
193 default = "CspRaw::default_blocked_uri",
194 alias = "blockedURL",
195 alias = "blocked-uri"
196 )]
197 blocked_uri: String,
198 #[serde(
199 skip_serializing_if = "Option::is_none",
200 alias = "documentURL",
201 alias = "document-uri"
202 )]
203 document_uri: Option<String>,
204 #[serde(
205 skip_serializing_if = "Option::is_none",
206 alias = "originalPolicy",
207 alias = "original-policy"
208 )]
209 original_policy: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 referrer: Option<String>,
212 #[serde(
213 default,
214 skip_serializing_if = "Option::is_none",
215 alias = "statusCode",
216 alias = "status-code",
217 deserialize_with = "de_opt_num_or_str"
218 )]
219 status_code: Option<u64>,
220 #[serde(
221 default = "String::new",
222 alias = "violatedDirective",
223 alias = "violated-directive"
224 )]
225 violated_directive: String,
226 #[serde(
227 skip_serializing_if = "Option::is_none",
228 alias = "sourceFile",
229 alias = "source-file"
230 )]
231 source_file: Option<String>,
232 #[serde(
233 default,
234 skip_serializing_if = "Option::is_none",
235 alias = "lineNumber",
236 alias = "line-number",
237 deserialize_with = "de_opt_num_or_str"
238 )]
239 line_number: Option<u64>,
240 #[serde(
241 default,
242 skip_serializing_if = "Option::is_none",
243 alias = "columnNumber",
244 alias = "column-number",
245 deserialize_with = "de_opt_num_or_str"
246 )]
247 column_number: Option<u64>,
248 #[serde(
249 skip_serializing_if = "Option::is_none",
250 alias = "scriptSample",
251 alias = "script-sample"
252 )]
253 script_sample: Option<String>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 disposition: Option<String>,
256
257 #[serde(flatten)]
258 other: BTreeMap<String, serde_json::Value>,
259}
260
261fn de_opt_num_or_str<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
262where
263 D: Deserializer<'de>,
264{
265 #[derive(Deserialize)]
266 #[serde(untagged)]
267 enum NumOrStr<'a> {
268 Num(u64),
269 Str(Cow<'a, str>),
270 }
271
272 Option::<NumOrStr>::deserialize(deserializer)?
273 .map(|status_code| match status_code {
274 NumOrStr::Num(num) => Ok(num),
275 NumOrStr::Str(s) => s.parse(),
276 })
277 .transpose()
278 .map_err(de::Error::custom)
279}
280
281impl CspRaw {
282 fn default_blocked_uri() -> String {
283 "self".to_string()
284 }
285
286 fn effective_directive(&self) -> Result<CspDirective, InvalidSecurityError> {
287 if let Some(directive) = &self.effective_directive {
292 if let Ok(parsed_directive) = directive
295 .split_once(' ')
296 .map_or(directive.as_str(), |s| s.0)
297 .parse()
298 {
299 return Ok(parsed_directive);
300 }
301 }
302
303 if let Ok(parsed_directive) = self
304 .violated_directive
305 .split_once(' ')
306 .map_or(self.violated_directive.as_str(), |s| s.0)
307 .parse()
308 {
309 Ok(parsed_directive)
310 } else {
311 Err(InvalidSecurityError)
312 }
313 }
314
315 fn get_message(&self, effective_directive: CspDirective) -> String {
316 if is_local(&self.blocked_uri) {
317 match effective_directive {
318 CspDirective::ChildSrc => "Blocked inline 'child'".to_string(),
319 CspDirective::ConnectSrc => "Blocked inline 'connect'".to_string(),
320 CspDirective::FontSrc => "Blocked inline 'font'".to_string(),
321 CspDirective::ImgSrc => "Blocked inline 'image'".to_string(),
322 CspDirective::ManifestSrc => "Blocked inline 'manifest'".to_string(),
323 CspDirective::MediaSrc => "Blocked inline 'media'".to_string(),
324 CspDirective::ObjectSrc => "Blocked inline 'object'".to_string(),
325 CspDirective::ScriptSrcAttr => "Blocked unsafe 'script' element".to_string(),
326 CspDirective::ScriptSrcElem => "Blocked inline script attribute".to_string(),
327 CspDirective::StyleSrc => "Blocked inline 'style'".to_string(),
328 CspDirective::StyleSrcElem => "Blocked 'style' or 'link' element".to_string(),
329 CspDirective::StyleSrcAttr => "Blocked style attribute".to_string(),
330 CspDirective::ScriptSrc => {
331 if self.violated_directive.contains("'unsafe-inline'") {
332 "Blocked unsafe inline 'script'".to_string()
333 } else if self.violated_directive.contains("'unsafe-eval'") {
334 "Blocked unsafe eval() 'script'".to_string()
335 } else {
336 "Blocked unsafe (eval() or inline) 'script'".to_string()
337 }
338 }
339 directive => format!("Blocked inline '{directive}'"),
340 }
341 } else {
342 let uri = normalize_uri(&self.blocked_uri);
343
344 match effective_directive {
345 CspDirective::ChildSrc => format!("Blocked 'child' from '{uri}'"),
346 CspDirective::ConnectSrc => format!("Blocked 'connect' from '{uri}'"),
347 CspDirective::FontSrc => format!("Blocked 'font' from '{uri}'"),
348 CspDirective::FormAction => format!("Blocked 'form' action to '{uri}'"),
349 CspDirective::ImgSrc => format!("Blocked 'image' from '{uri}'"),
350 CspDirective::ManifestSrc => format!("Blocked 'manifest' from '{uri}'"),
351 CspDirective::MediaSrc => format!("Blocked 'media' from '{uri}'"),
352 CspDirective::ObjectSrc => format!("Blocked 'object' from '{uri}'"),
353 CspDirective::ScriptSrc => format!("Blocked 'script' from '{uri}'"),
354 CspDirective::ScriptSrcAttr => {
355 format!("Blocked inline script attribute from '{uri}'")
356 }
357 CspDirective::ScriptSrcElem => format!("Blocked 'script' from '{uri}'"),
358 CspDirective::StyleSrc => format!("Blocked 'style' from '{uri}'"),
359 CspDirective::StyleSrcElem => format!("Blocked 'style' from '{uri}'"),
360 CspDirective::StyleSrcAttr => format!("Blocked style attribute from '{uri}'"),
361 directive => format!("Blocked '{directive}' from '{uri}'"),
362 }
363 }
364 }
365
366 fn into_protocol(self, effective_directive: CspDirective) -> Csp {
367 Csp {
368 effective_directive: Annotated::from(effective_directive.to_string()),
369 blocked_uri: Annotated::from(self.blocked_uri),
370 document_uri: Annotated::from(self.document_uri),
371 original_policy: Annotated::from(self.original_policy),
372 referrer: Annotated::from(self.referrer),
373 status_code: Annotated::from(self.status_code),
374 violated_directive: Annotated::from(self.violated_directive),
375 source_file: Annotated::from(self.source_file),
376 line_number: Annotated::from(self.line_number),
377 column_number: Annotated::from(self.column_number),
378 script_sample: Annotated::from(self.script_sample),
379 disposition: Annotated::from(self.disposition),
380 other: self
381 .other
382 .into_iter()
383 .map(|(k, v)| (k, Annotated::from(v)))
384 .collect(),
385 }
386 }
387
388 fn sanitized_blocked_uri(&self) -> String {
389 let mut uri = self.blocked_uri.clone();
397
398 if uri.starts_with("https://api.stripe.com/") {
399 if let Some(index) = uri.find(&['#', '?'][..]) {
400 uri.truncate(index);
401 }
402 }
403
404 uri
405 }
406
407 fn normalize_value<'a>(&self, value: &'a str, document_uri: &str) -> Cow<'a, str> {
408 if let "'none'" | "'self'" | "'unsafe-inline'" | "'unsafe-eval'" = value {
412 return Cow::Borrowed(value);
413 }
414
415 if value.starts_with("data:")
419 || value.starts_with("mediastream:")
420 || value.starts_with("blob:")
421 || value.starts_with("filesystem:")
422 || value.starts_with("http:")
423 || value.starts_with("https:")
424 || value.starts_with("file:")
425 {
426 if document_uri == normalize_uri(value) {
427 return Cow::Borrowed("'self'");
428 }
429
430 return Cow::Borrowed(value);
432 }
433
434 if value == document_uri {
437 return Cow::Borrowed("'self'");
438 }
439
440 let original_uri = self.document_uri.as_deref().unwrap_or_default();
443 match original_uri.split_once(':').map(|x| x.0) {
444 None | Some("http" | "https") => Cow::Borrowed(value),
445 Some(scheme) => Cow::Owned(unsplit_uri(scheme, value)),
446 }
447 }
448
449 fn get_culprit(&self) -> String {
450 if self.violated_directive.is_empty() {
451 return String::new();
452 }
453
454 let mut bits = self.violated_directive.split_ascii_whitespace();
455 let mut culprit = bits.next().unwrap_or_default().to_owned();
456
457 let document_uri = self.document_uri.as_deref().unwrap_or("");
458 let normalized_uri = normalize_uri(document_uri);
459
460 for bit in bits {
461 write!(culprit, " {}", self.normalize_value(bit, &normalized_uri)).ok();
462 }
463
464 culprit
465 }
466
467 fn get_tags(&self, effective_directive: CspDirective) -> Tags {
468 let mut tags = vec![
469 Annotated::new(TagEntry(
470 Annotated::new("effective-directive".to_string()),
471 Annotated::new(effective_directive.to_string()),
472 )),
473 Annotated::new(TagEntry(
474 Annotated::new("blocked-uri".to_string()),
475 Annotated::new(self.sanitized_blocked_uri()),
476 )),
477 ];
478
479 if let Ok(url) = Url::parse(&self.blocked_uri) {
480 if let ("http" | "https", Some(host)) = (url.scheme(), url.host_str()) {
481 tags.push(Annotated::new(TagEntry(
482 Annotated::new("blocked-host".to_string()),
483 Annotated::new(host.to_owned()),
484 )));
485 }
486 }
487
488 Tags(PairList::from(tags))
489 }
490
491 fn get_request(&self) -> Request {
492 let headers = match self.referrer {
493 Some(ref referrer) if !referrer.is_empty() => {
494 Annotated::new(Headers(PairList(vec![Annotated::new((
495 Annotated::new(HeaderName::new("Referer")),
496 Annotated::new(HeaderValue::new(referrer.clone())),
497 ))])))
498 }
499 Some(_) | None => Annotated::empty(),
500 };
501
502 Request {
503 url: Annotated::from(self.document_uri.clone()),
504 headers,
505 ..Request::default()
506 }
507 }
508}
509
510#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
514#[serde(rename_all = "kebab-case")]
515struct CspReportRaw {
516 csp_report: CspRaw,
517}
518
519#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
520#[serde(untagged)]
521enum CspVariant {
522 Csp {
523 #[serde(rename = "csp-report")]
524 csp_report: CspRaw,
525 },
526 CspViolation { body: CspRaw },
533}
534
535#[derive(Clone, Debug, PartialEq, Deserialize)]
537#[serde(rename_all = "kebab-case")]
538enum CspViolationType {
539 CspViolation,
540 #[serde(other)]
541 Other,
542}
543
544#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
556pub struct Csp {
557 #[metastructure(pii = "true")]
559 pub effective_directive: Annotated<String>,
560 #[metastructure(pii = "true")]
562 pub blocked_uri: Annotated<String>,
563 #[metastructure(pii = "true")]
565 pub document_uri: Annotated<String>,
566 pub original_policy: Annotated<String>,
568 #[metastructure(pii = "true")]
570 pub referrer: Annotated<String>,
571 pub status_code: Annotated<u64>,
573 pub violated_directive: Annotated<String>,
575 pub source_file: Annotated<String>,
577 pub line_number: Annotated<u64>,
579 pub column_number: Annotated<u64>,
581 pub script_sample: Annotated<String>,
584 pub disposition: Annotated<String>,
586 #[metastructure(pii = "true", additional_properties)]
588 pub other: Object<Value>,
589}
590
591impl Csp {
592 pub fn apply_to_event(data: &[u8], event: &mut Event) -> Result<(), serde_json::Error> {
593 let variant = serde_json::from_slice::<CspVariant>(data)?;
594 match variant {
595 CspVariant::Csp { csp_report } => Csp::extract_report(event, csp_report)?,
596 CspVariant::CspViolation { body } => Csp::extract_report(event, body)?,
597 }
598
599 Ok(())
600 }
601
602 fn extract_report(event: &mut Event, raw_csp: CspRaw) -> Result<(), serde_json::Error> {
603 let effective_directive = raw_csp
604 .effective_directive()
605 .map_err(serde::de::Error::custom)?;
606
607 event.logentry = Annotated::new(LogEntry::from(raw_csp.get_message(effective_directive)));
608 event.culprit = Annotated::new(raw_csp.get_culprit());
609 event.tags = Annotated::new(raw_csp.get_tags(effective_directive));
610 event.request = Annotated::new(raw_csp.get_request());
611 event.csp = Annotated::new(raw_csp.into_protocol(effective_directive));
612
613 Ok(())
614 }
615}
616
617#[derive(Clone, Copy, Debug, PartialEq, Eq)]
618enum ExpectCtStatus {
619 Unknown,
620 Valid,
621 Invalid,
622}
623
624relay_common::derive_fromstr_and_display!(ExpectCtStatus, InvalidSecurityError, {
625 ExpectCtStatus::Unknown => "unknown",
626 ExpectCtStatus::Valid => "valid",
627 ExpectCtStatus::Invalid => "invalid",
628});
629
630relay_common::impl_str_serde!(ExpectCtStatus, "an expect-ct status");
631
632#[derive(Clone, Copy, Debug, PartialEq, Eq)]
633enum ExpectCtSource {
634 TlsExtension,
635 Ocsp,
636 Embedded,
637}
638
639relay_common::derive_fromstr_and_display!(ExpectCtSource, InvalidSecurityError, {
640 ExpectCtSource::TlsExtension => "tls-extension",
641 ExpectCtSource::Ocsp => "ocsp",
642 ExpectCtSource::Embedded => "embedded",
643});
644
645relay_common::impl_str_serde!(ExpectCtSource, "an expect-ct source");
646
647#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
648struct SingleCertificateTimestampRaw {
649 version: Option<i64>,
650 status: Option<ExpectCtStatus>,
651 source: Option<ExpectCtSource>,
652 serialized_sct: Option<String>, }
654
655impl SingleCertificateTimestampRaw {
656 fn into_protocol(self) -> SingleCertificateTimestamp {
657 SingleCertificateTimestamp {
658 version: Annotated::from(self.version),
659 status: Annotated::from(self.status.map(|s| s.to_string())),
660 source: Annotated::from(self.source.map(|s| s.to_string())),
661 serialized_sct: Annotated::from(self.serialized_sct),
662 }
663 }
664}
665
666#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
667#[serde(rename_all = "kebab-case")]
668struct ExpectCtRaw {
669 #[serde(with = "serde_date_time_3339")]
670 date_time: Option<DateTime<Utc>>,
671 hostname: String,
672 port: Option<i64>,
673 scheme: Option<String>,
674 #[serde(with = "serde_date_time_3339")]
675 effective_expiration_date: Option<DateTime<Utc>>,
676 served_certificate_chain: Option<Vec<String>>,
677 validated_certificate_chain: Option<Vec<String>>,
678 scts: Option<Vec<SingleCertificateTimestampRaw>>,
679 failure_mode: Option<String>,
680 test_report: Option<bool>,
681}
682
683mod serde_date_time_3339 {
684 use serde::de::Visitor;
685
686 use super::*;
687
688 pub fn serialize<S>(date_time: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
689 where
690 S: serde::Serializer,
691 {
692 match date_time {
693 None => serializer.serialize_none(),
694 Some(d) => serializer.serialize_str(&d.to_rfc3339()),
695 }
696 }
697
698 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
699 where
700 D: serde::Deserializer<'de>,
701 {
702 struct DateTimeVisitor;
703
704 impl Visitor<'_> for DateTimeVisitor {
705 type Value = Option<DateTime<Utc>>;
706
707 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
708 formatter.write_str("expected a date-time in RFC 3339 format")
709 }
710
711 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
712 where
713 E: serde::de::Error,
714 {
715 DateTime::parse_from_rfc3339(s)
716 .map(|d| Some(d.with_timezone(&Utc)))
717 .map_err(serde::de::Error::custom)
718 }
719
720 fn visit_none<E>(self) -> Result<Self::Value, E>
721 where
722 E: Error,
723 {
724 Ok(None)
725 }
726 }
727
728 deserializer.deserialize_any(DateTimeVisitor)
729 }
730}
731
732impl ExpectCtRaw {
733 fn get_message(&self) -> String {
734 format!("Expect-CT failed for '{}'", self.hostname)
735 }
736
737 fn into_protocol(self) -> ExpectCt {
738 ExpectCt {
739 date_time: Annotated::from(self.date_time.map(|d| d.to_rfc3339())),
740 hostname: Annotated::from(self.hostname),
741 port: Annotated::from(self.port),
742 scheme: Annotated::from(self.scheme),
743 effective_expiration_date: Annotated::from(
744 self.effective_expiration_date.map(|d| d.to_rfc3339()),
745 ),
746 served_certificate_chain: Annotated::new(
747 self.served_certificate_chain
748 .map(|s| s.into_iter().map(Annotated::from).collect())
749 .unwrap_or_default(),
750 ),
751
752 validated_certificate_chain: Annotated::new(
753 self.validated_certificate_chain
754 .map(|v| v.into_iter().map(Annotated::from).collect())
755 .unwrap_or_default(),
756 ),
757 scts: Annotated::from(self.scts.map(|scts| {
758 scts.into_iter()
759 .map(|elm| Annotated::from(elm.into_protocol()))
760 .collect()
761 })),
762 failure_mode: Annotated::from(self.failure_mode),
763 test_report: Annotated::from(self.test_report),
764 }
765 }
766
767 fn get_culprit(&self) -> String {
768 self.hostname.clone()
769 }
770
771 fn get_tags(&self) -> Tags {
772 let mut tags = vec![Annotated::new(TagEntry(
773 Annotated::new("hostname".to_string()),
774 Annotated::new(self.hostname.clone()),
775 ))];
776
777 if let Some(port) = self.port {
778 tags.push(Annotated::new(TagEntry(
779 Annotated::new("port".to_string()),
780 Annotated::new(port.to_string()),
781 )));
782 }
783
784 Tags(PairList::from(tags))
785 }
786
787 fn get_request(&self) -> Request {
788 Request {
789 url: Annotated::from(self.hostname.clone()),
790 ..Request::default()
791 }
792 }
793}
794
795#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
796#[serde(rename_all = "kebab-case")]
797struct ExpectCtReportRaw {
798 expect_ct_report: ExpectCtRaw,
799}
800
801#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
805pub struct SingleCertificateTimestamp {
806 pub version: Annotated<i64>,
807 pub status: Annotated<String>,
808 pub source: Annotated<String>,
809 pub serialized_sct: Annotated<String>,
810}
811
812#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
816pub struct ExpectCt {
817 pub date_time: Annotated<String>,
820 pub hostname: Annotated<String>,
822 pub port: Annotated<i64>,
823 pub scheme: Annotated<String>,
824 pub effective_expiration_date: Annotated<String>,
826 pub served_certificate_chain: Annotated<Array<String>>,
827 pub validated_certificate_chain: Annotated<Array<String>>,
828 pub scts: Annotated<Array<SingleCertificateTimestamp>>,
829 pub failure_mode: Annotated<String>,
830 pub test_report: Annotated<bool>,
831}
832
833impl ExpectCt {
834 pub fn apply_to_event(data: &[u8], event: &mut Event) -> Result<(), serde_json::Error> {
835 let raw_report = serde_json::from_slice::<ExpectCtReportRaw>(data)?;
836 let raw_expect_ct = raw_report.expect_ct_report;
837
838 event.logentry = Annotated::new(LogEntry::from(raw_expect_ct.get_message()));
839 event.culprit = Annotated::new(raw_expect_ct.get_culprit());
840 event.tags = Annotated::new(raw_expect_ct.get_tags());
841 event.request = Annotated::new(raw_expect_ct.get_request());
842 event.expectct = Annotated::new(raw_expect_ct.into_protocol());
843
844 Ok(())
845 }
846}
847
848#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
852#[serde(rename_all = "kebab-case")]
853struct HpkpRaw {
854 #[serde(skip_serializing_if = "Option::is_none")]
855 date_time: Option<DateTime<Utc>>,
856 hostname: String,
857 #[serde(skip_serializing_if = "Option::is_none")]
858 port: Option<u64>,
859 #[serde(skip_serializing_if = "Option::is_none")]
860 effective_expiration_date: Option<DateTime<Utc>>,
861 #[serde(skip_serializing_if = "Option::is_none")]
862 include_subdomains: Option<bool>,
863 #[serde(skip_serializing_if = "Option::is_none")]
864 noted_hostname: Option<String>,
865 #[serde(skip_serializing_if = "Option::is_none")]
866 served_certificate_chain: Option<Vec<String>>,
867 #[serde(skip_serializing_if = "Option::is_none")]
868 validated_certificate_chain: Option<Vec<String>>,
869 known_pins: Vec<String>,
870 #[serde(flatten)]
871 other: BTreeMap<String, serde_json::Value>,
872}
873
874impl HpkpRaw {
875 fn get_message(&self) -> String {
876 format!(
877 "Public key pinning validation failed for '{}'",
878 self.hostname
879 )
880 }
881
882 fn into_protocol(self) -> Hpkp {
883 Hpkp {
884 date_time: Annotated::from(self.date_time.map(|d| d.to_rfc3339())),
885 hostname: Annotated::new(self.hostname),
886 port: Annotated::from(self.port),
887 effective_expiration_date: Annotated::from(
888 self.effective_expiration_date.map(|d| d.to_rfc3339()),
889 ),
890 include_subdomains: Annotated::from(self.include_subdomains),
891 noted_hostname: Annotated::from(self.noted_hostname),
892 served_certificate_chain: Annotated::from(
893 self.served_certificate_chain
894 .map(|chain| chain.into_iter().map(Annotated::from).collect()),
895 ),
896 validated_certificate_chain: Annotated::from(
897 self.validated_certificate_chain
898 .map(|chain| chain.into_iter().map(Annotated::from).collect()),
899 ),
900 known_pins: Annotated::new(self.known_pins.into_iter().map(Annotated::from).collect()),
901 other: self
902 .other
903 .into_iter()
904 .map(|(k, v)| (k, Annotated::from(v)))
905 .collect(),
906 }
907 }
908
909 fn get_tags(&self) -> Tags {
910 let mut tags = vec![Annotated::new(TagEntry(
911 Annotated::new("hostname".to_string()),
912 Annotated::new(self.hostname.clone()),
913 ))];
914
915 if let Some(port) = self.port {
916 tags.push(Annotated::new(TagEntry(
917 Annotated::new("port".to_string()),
918 Annotated::new(port.to_string()),
919 )));
920 }
921
922 if let Some(include_subdomains) = self.include_subdomains {
923 tags.push(Annotated::new(TagEntry(
924 Annotated::new("include-subdomains".to_string()),
925 Annotated::new(include_subdomains.to_string()),
926 )));
927 }
928
929 Tags(PairList::from(tags))
930 }
931
932 fn get_request(&self) -> Request {
933 Request {
934 url: Annotated::from(self.hostname.clone()),
935 ..Request::default()
936 }
937 }
938}
939
940#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
942pub struct Hpkp {
943 pub date_time: Annotated<String>,
945 pub hostname: Annotated<String>,
947 pub port: Annotated<u64>,
949 pub effective_expiration_date: Annotated<String>,
951 pub include_subdomains: Annotated<bool>,
954
955 pub noted_hostname: Annotated<String>,
959 pub served_certificate_chain: Annotated<Array<String>>,
965 pub validated_certificate_chain: Annotated<Array<String>>,
967
968 #[metastructure(required = true)]
971 pub known_pins: Annotated<Array<String>>,
972
973 #[metastructure(pii = "true", additional_properties)]
974 pub other: Object<Value>,
975}
976
977impl Hpkp {
978 pub fn apply_to_event(data: &[u8], event: &mut Event) -> Result<(), serde_json::Error> {
979 let raw_hpkp = serde_json::from_slice::<HpkpRaw>(data)?;
980
981 event.logentry = Annotated::new(LogEntry::from(raw_hpkp.get_message()));
982 event.tags = Annotated::new(raw_hpkp.get_tags());
983 event.request = Annotated::new(raw_hpkp.get_request());
984 event.hpkp = Annotated::new(raw_hpkp.into_protocol());
985
986 Ok(())
987 }
988}
989
990#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
994#[serde(rename_all = "kebab-case")]
995struct ExpectStapleReportRaw {
996 expect_staple_report: ExpectStapleRaw,
997}
998
999#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1000pub enum ExpectStapleResponseStatus {
1001 Missing,
1002 Provided,
1003 ErrorResponse,
1004 BadProducedAt,
1005 NoMatchingResponse,
1006 InvalidDate,
1007 ParseResponseError,
1008 ParseResponseDataError,
1009}
1010
1011relay_common::derive_fromstr_and_display!(ExpectStapleResponseStatus, InvalidSecurityError, {
1012 ExpectStapleResponseStatus::Missing => "MISSING",
1013 ExpectStapleResponseStatus::Provided => "PROVIDED",
1014 ExpectStapleResponseStatus::ErrorResponse => "ERROR_RESPONSE",
1015 ExpectStapleResponseStatus::BadProducedAt => "BAD_PRODUCED_AT",
1016 ExpectStapleResponseStatus::NoMatchingResponse => "NO_MATCHING_RESPONSE",
1017 ExpectStapleResponseStatus::InvalidDate => "INVALID_DATE",
1018 ExpectStapleResponseStatus::ParseResponseError => "PARSE_RESPONSE_ERROR",
1019 ExpectStapleResponseStatus::ParseResponseDataError => "PARSE_RESPONSE_DATA_ERROR",
1020});
1021
1022relay_common::impl_str_serde!(ExpectStapleResponseStatus, "an expect-ct response status");
1023
1024#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1025pub enum ExpectStapleCertStatus {
1026 Good,
1027 Revoked,
1028 Unknown,
1029}
1030
1031relay_common::derive_fromstr_and_display!(ExpectStapleCertStatus, InvalidSecurityError, {
1032 ExpectStapleCertStatus::Good => "GOOD",
1033 ExpectStapleCertStatus::Revoked => "REVOKED",
1034 ExpectStapleCertStatus::Unknown => "UNKNOWN",
1035});
1036
1037relay_common::impl_str_serde!(ExpectStapleCertStatus, "an expect-staple cert status");
1038
1039#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
1041#[serde(rename_all = "kebab-case")]
1042struct ExpectStapleRaw {
1043 #[serde(skip_serializing_if = "Option::is_none")]
1044 date_time: Option<DateTime<Utc>>,
1045 hostname: String,
1046 #[serde(skip_serializing_if = "Option::is_none")]
1047 port: Option<i64>,
1048 #[serde(skip_serializing_if = "Option::is_none")]
1049 effective_expiration_date: Option<DateTime<Utc>>,
1050 #[serde(skip_serializing_if = "Option::is_none")]
1051 response_status: Option<ExpectStapleResponseStatus>,
1052 #[serde(skip_serializing_if = "Option::is_none")]
1053 ocsp_response: Option<Value>,
1054 #[serde(skip_serializing_if = "Option::is_none")]
1055 cert_status: Option<ExpectStapleCertStatus>,
1056 #[serde(skip_serializing_if = "Option::is_none")]
1057 served_certificate_chain: Option<Vec<String>>,
1058 #[serde(skip_serializing_if = "Option::is_none")]
1059 validated_certificate_chain: Option<Vec<String>>,
1060}
1061
1062impl ExpectStapleRaw {
1063 fn get_message(&self) -> String {
1064 format!("Expect-Staple failed for '{}'", self.hostname)
1065 }
1066
1067 fn into_protocol(self) -> ExpectStaple {
1068 ExpectStaple {
1069 date_time: Annotated::from(self.date_time.map(|d| d.to_rfc3339())),
1070 hostname: Annotated::from(self.hostname),
1071 port: Annotated::from(self.port),
1072 effective_expiration_date: Annotated::from(
1073 self.effective_expiration_date.map(|d| d.to_rfc3339()),
1074 ),
1075 response_status: Annotated::from(self.response_status.map(|rs| rs.to_string())),
1076 cert_status: Annotated::from(self.cert_status.map(|cs| cs.to_string())),
1077 served_certificate_chain: Annotated::from(
1078 self.served_certificate_chain
1079 .map(|cert_chain| cert_chain.into_iter().map(Annotated::from).collect()),
1080 ),
1081 validated_certificate_chain: Annotated::from(
1082 self.validated_certificate_chain
1083 .map(|cert_chain| cert_chain.into_iter().map(Annotated::from).collect()),
1084 ),
1085 ocsp_response: Annotated::from(self.ocsp_response),
1086 }
1087 }
1088
1089 fn get_culprit(&self) -> String {
1090 self.hostname.clone()
1091 }
1092
1093 fn get_tags(&self) -> Tags {
1094 let mut tags = vec![Annotated::new(TagEntry(
1095 Annotated::new("hostname".to_string()),
1096 Annotated::new(self.hostname.clone()),
1097 ))];
1098
1099 if let Some(port) = self.port {
1100 tags.push(Annotated::new(TagEntry(
1101 Annotated::new("port".to_string()),
1102 Annotated::new(port.to_string()),
1103 )));
1104 }
1105
1106 if let Some(response_status) = self.response_status {
1107 tags.push(Annotated::new(TagEntry(
1108 Annotated::new("response_status".to_string()),
1109 Annotated::new(response_status.to_string()),
1110 )));
1111 }
1112
1113 if let Some(cert_status) = self.cert_status {
1114 tags.push(Annotated::new(TagEntry(
1115 Annotated::new("cert_status".to_string()),
1116 Annotated::new(cert_status.to_string()),
1117 )));
1118 }
1119
1120 Tags(PairList::from(tags))
1121 }
1122
1123 fn get_request(&self) -> Request {
1124 Request {
1125 url: Annotated::from(self.hostname.clone()),
1126 ..Request::default()
1127 }
1128 }
1129}
1130
1131#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
1135pub struct ExpectStaple {
1136 date_time: Annotated<String>,
1137 hostname: Annotated<String>,
1138 port: Annotated<i64>,
1139 effective_expiration_date: Annotated<String>,
1140 response_status: Annotated<String>,
1141 cert_status: Annotated<String>,
1142 served_certificate_chain: Annotated<Array<String>>,
1143 validated_certificate_chain: Annotated<Array<String>>,
1144 ocsp_response: Annotated<Value>,
1145}
1146
1147impl ExpectStaple {
1148 pub fn apply_to_event(data: &[u8], event: &mut Event) -> Result<(), serde_json::Error> {
1149 let raw_report = serde_json::from_slice::<ExpectStapleReportRaw>(data)?;
1150 let raw_expect_staple = raw_report.expect_staple_report;
1151
1152 event.logentry = Annotated::new(LogEntry::from(raw_expect_staple.get_message()));
1153 event.culprit = Annotated::new(raw_expect_staple.get_culprit());
1154 event.tags = Annotated::new(raw_expect_staple.get_tags());
1155 event.request = Annotated::new(raw_expect_staple.get_request());
1156 event.expectstaple = Annotated::new(raw_expect_staple.into_protocol());
1157
1158 Ok(())
1159 }
1160}
1161
1162#[derive(Clone, Debug, PartialEq, Eq)]
1163pub enum SecurityReportType {
1164 Csp,
1165 ExpectCt,
1166 ExpectStaple,
1167 Hpkp,
1168 Unsupported,
1169}
1170
1171impl SecurityReportType {
1172 pub fn from_json(data: &[u8]) -> Result<Option<Self>, serde_json::Error> {
1177 #[derive(Deserialize)]
1178 #[serde(rename_all = "kebab-case")]
1179 struct SecurityReport {
1180 #[serde(rename = "type")]
1181 ty: Option<CspViolationType>,
1182 csp_report: Option<IgnoredAny>,
1183 known_pins: Option<IgnoredAny>,
1184 expect_staple_report: Option<IgnoredAny>,
1185 expect_ct_report: Option<IgnoredAny>,
1186 }
1187
1188 let helper: SecurityReport = serde_json::from_slice(data)?;
1189
1190 Ok(if helper.csp_report.is_some() {
1191 Some(SecurityReportType::Csp)
1192 } else if let Some(CspViolationType::CspViolation) = helper.ty {
1193 Some(SecurityReportType::Csp)
1194 } else if let Some(CspViolationType::Other) = helper.ty {
1195 Some(SecurityReportType::Unsupported)
1196 } else if helper.known_pins.is_some() {
1197 Some(SecurityReportType::Hpkp)
1198 } else if helper.expect_staple_report.is_some() {
1199 Some(SecurityReportType::ExpectStaple)
1200 } else if helper.expect_ct_report.is_some() {
1201 Some(SecurityReportType::ExpectCt)
1202 } else {
1203 None
1204 })
1205 }
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210 use super::*;
1211 use relay_protocol::assert_annotated_snapshot;
1212
1213 #[test]
1214 fn test_unsplit_uri() {
1215 assert_eq!(unsplit_uri("", ""), "");
1216 assert_eq!(unsplit_uri("data", ""), "data:");
1217 assert_eq!(unsplit_uri("data", "foo"), "data://foo");
1218 assert_eq!(unsplit_uri("http", ""), "http://");
1219 assert_eq!(unsplit_uri("http", "foo"), "http://foo");
1220 }
1221
1222 #[test]
1223 fn test_normalize_uri() {
1224 assert_eq!(normalize_uri(""), "'self'");
1226 assert_eq!(normalize_uri("self"), "'self'");
1227
1228 assert_eq!(normalize_uri("data"), "data:");
1230 assert_eq!(normalize_uri("http"), "http://");
1231
1232 assert_eq!(normalize_uri("http://notlocalhost/"), "notlocalhost");
1234 assert_eq!(normalize_uri("https://notlocalhost/"), "notlocalhost");
1235 assert_eq!(normalize_uri("data://notlocalhost/"), "data://notlocalhost");
1236 assert_eq!(normalize_uri("http://notlocalhost/lol.css"), "notlocalhost");
1237
1238 assert_eq!(
1240 normalize_uri("http://notlocalhost:8000/"),
1241 "notlocalhost:8000"
1242 );
1243 assert_eq!(
1244 normalize_uri("http://notlocalhost:8000/lol.css"),
1245 "notlocalhost:8000"
1246 );
1247
1248 assert_eq!(normalize_uri("xyz://notlocalhost/"), "xyz://notlocalhost");
1250 }
1251
1252 #[test]
1253 fn test_csp_basic() {
1254 let json = r#"{
1255 "csp-report": {
1256 "document-uri": "http://example.com",
1257 "violated-directive": "style-src cdn.example.com",
1258 "blocked-uri": "http://example.com/lol.css",
1259 "effective-directive": "style-src",
1260 "status-code": "200"
1261 }
1262 }"#;
1263
1264 let mut event = Event::default();
1265 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1266
1267 assert_annotated_snapshot!(Annotated::new(event), @r###"
1268 {
1269 "culprit": "style-src cdn.example.com",
1270 "logentry": {
1271 "formatted": "Blocked 'style' from 'example.com'"
1272 },
1273 "request": {
1274 "url": "http://example.com"
1275 },
1276 "tags": [
1277 [
1278 "effective-directive",
1279 "style-src"
1280 ],
1281 [
1282 "blocked-uri",
1283 "http://example.com/lol.css"
1284 ],
1285 [
1286 "blocked-host",
1287 "example.com"
1288 ]
1289 ],
1290 "csp": {
1291 "effective_directive": "style-src",
1292 "blocked_uri": "http://example.com/lol.css",
1293 "document_uri": "http://example.com",
1294 "status_code": 200,
1295 "violated_directive": "style-src cdn.example.com"
1296 }
1297 }
1298 "###);
1299 }
1300
1301 #[test]
1302 fn test_csp_coerce_blocked_uri_if_missing() {
1303 let json = r#"{
1304 "csp-report": {
1305 "document-uri": "http://example.com",
1306 "effective-directive": "script-src"
1307 }
1308 }"#;
1309
1310 let mut event = Event::default();
1311 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1312
1313 assert_annotated_snapshot!(Annotated::new(event), @r#"
1314 {
1315 "culprit": "",
1316 "logentry": {
1317 "formatted": "Blocked unsafe (eval() or inline) 'script'"
1318 },
1319 "request": {
1320 "url": "http://example.com"
1321 },
1322 "tags": [
1323 [
1324 "effective-directive",
1325 "script-src"
1326 ],
1327 [
1328 "blocked-uri",
1329 "self"
1330 ]
1331 ],
1332 "csp": {
1333 "effective_directive": "script-src",
1334 "blocked_uri": "self",
1335 "document_uri": "http://example.com",
1336 "violated_directive": ""
1337 }
1338 }
1339 "#);
1340 }
1341
1342 #[test]
1343 fn test_csp_msdn() {
1344 let json = r#"{
1345 "csp-report": {
1346 "document-uri": "https://example.com/foo/bar",
1347 "referrer": "https://www.google.com/",
1348 "violated-directive": "default-src self",
1349 "original-policy": "default-src self; report-uri /csp-hotline.php",
1350 "blocked-uri": "http://evilhackerscripts.com"
1351 }
1352 }"#;
1353
1354 let mut event = Event::default();
1355 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1356
1357 assert_annotated_snapshot!(Annotated::new(event), @r###"
1358 {
1359 "culprit": "default-src self",
1360 "logentry": {
1361 "formatted": "Blocked 'default-src' from 'evilhackerscripts.com'"
1362 },
1363 "request": {
1364 "url": "https://example.com/foo/bar",
1365 "headers": [
1366 [
1367 "Referer",
1368 "https://www.google.com/"
1369 ]
1370 ]
1371 },
1372 "tags": [
1373 [
1374 "effective-directive",
1375 "default-src"
1376 ],
1377 [
1378 "blocked-uri",
1379 "http://evilhackerscripts.com"
1380 ],
1381 [
1382 "blocked-host",
1383 "evilhackerscripts.com"
1384 ]
1385 ],
1386 "csp": {
1387 "effective_directive": "default-src",
1388 "blocked_uri": "http://evilhackerscripts.com",
1389 "document_uri": "https://example.com/foo/bar",
1390 "original_policy": "default-src self; report-uri /csp-hotline.php",
1391 "referrer": "https://www.google.com/",
1392 "violated_directive": "default-src self"
1393 }
1394 }
1395 "###);
1396 }
1397
1398 #[test]
1399 fn test_csp_real() {
1400 let json = r#"{
1401 "csp-report": {
1402 "document-uri": "https://sentry.io/sentry/csp/issues/88513416/",
1403 "referrer": "https://sentry.io/sentry/sentry/releases/7329107476ff14cfa19cf013acd8ce47781bb93a/",
1404 "violated-directive": "script-src",
1405 "effective-directive": "script-src",
1406 "original-policy": "default-src *; script-src 'make_csp_snapshot' 'unsafe-eval' 'unsafe-inline' e90d271df3e973c7.global.ssl.fastly.net cdn.ravenjs.com assets.zendesk.com ajax.googleapis.com ssl.google-analytics.com www.googleadservices.com analytics.twitter.com platform.twitter.com *.pingdom.net js.stripe.com api.stripe.com statuspage-production.s3.amazonaws.com s3.amazonaws.com *.google.com www.gstatic.com aui-cdn.atlassian.com *.atlassian.net *.jira.com *.zopim.com; font-src * data:; connect-src * wss://*.zopim.com; style-src 'make_csp_snapshot' 'unsafe-inline' e90d271df3e973c7.global.ssl.fastly.net s3.amazonaws.com aui-cdn.atlassian.com fonts.googleapis.com; img-src * data: blob:; report-uri https://sentry.io/api/54785/csp-report/?sentry_key=f724a8a027db45f5b21507e7142ff78e&sentry_release=39662eb9734f68e56b7f202260bb706be2f4cee7",
1407 "disposition": "enforce",
1408 "blocked-uri": "http://baddomain.com/test.js?_=1515535030116",
1409 "line-number": 24,
1410 "column-number": 66270,
1411 "source-file": "https://e90d271df3e973c7.global.ssl.fastly.net/_static/f0c7c026a4b2a3d2b287ae2d012c9924/sentry/dist/vendor.js",
1412 "status-code": 0,
1413 "script-sample": ""
1414 }
1415 }"#;
1416
1417 let mut event = Event::default();
1418 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1419
1420 assert_annotated_snapshot!(Annotated::new(event), @r###"
1421 {
1422 "culprit": "script-src",
1423 "logentry": {
1424 "formatted": "Blocked 'script' from 'baddomain.com'"
1425 },
1426 "request": {
1427 "url": "https://sentry.io/sentry/csp/issues/88513416/",
1428 "headers": [
1429 [
1430 "Referer",
1431 "https://sentry.io/sentry/sentry/releases/7329107476ff14cfa19cf013acd8ce47781bb93a/"
1432 ]
1433 ]
1434 },
1435 "tags": [
1436 [
1437 "effective-directive",
1438 "script-src"
1439 ],
1440 [
1441 "blocked-uri",
1442 "http://baddomain.com/test.js?_=1515535030116"
1443 ],
1444 [
1445 "blocked-host",
1446 "baddomain.com"
1447 ]
1448 ],
1449 "csp": {
1450 "effective_directive": "script-src",
1451 "blocked_uri": "http://baddomain.com/test.js?_=1515535030116",
1452 "document_uri": "https://sentry.io/sentry/csp/issues/88513416/",
1453 "original_policy": "default-src *; script-src 'make_csp_snapshot' 'unsafe-eval' 'unsafe-inline' e90d271df3e973c7.global.ssl.fastly.net cdn.ravenjs.com assets.zendesk.com ajax.googleapis.com ssl.google-analytics.com www.googleadservices.com analytics.twitter.com platform.twitter.com *.pingdom.net js.stripe.com api.stripe.com statuspage-production.s3.amazonaws.com s3.amazonaws.com *.google.com www.gstatic.com aui-cdn.atlassian.com *.atlassian.net *.jira.com *.zopim.com; font-src * data:; connect-src * wss://*.zopim.com; style-src 'make_csp_snapshot' 'unsafe-inline' e90d271df3e973c7.global.ssl.fastly.net s3.amazonaws.com aui-cdn.atlassian.com fonts.googleapis.com; img-src * data: blob:; report-uri https://sentry.io/api/54785/csp-report/?sentry_key=f724a8a027db45f5b21507e7142ff78e&sentry_release=39662eb9734f68e56b7f202260bb706be2f4cee7",
1454 "referrer": "https://sentry.io/sentry/sentry/releases/7329107476ff14cfa19cf013acd8ce47781bb93a/",
1455 "status_code": 0,
1456 "violated_directive": "script-src",
1457 "source_file": "https://e90d271df3e973c7.global.ssl.fastly.net/_static/f0c7c026a4b2a3d2b287ae2d012c9924/sentry/dist/vendor.js",
1458 "line_number": 24,
1459 "column_number": 66270,
1460 "script_sample": "",
1461 "disposition": "enforce"
1462 }
1463 }
1464 "###);
1465 }
1466
1467 #[test]
1468 fn test_csp_culprit_0() {
1469 let json = r#"{
1470 "csp-report": {
1471 "document-uri": "http://example.com/foo",
1472 "violated-directive": "style-src http://cdn.example.com",
1473 "effective-directive": "style-src"
1474 }
1475 }"#;
1476
1477 let mut event = Event::default();
1478 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1479 insta::assert_debug_snapshot!(event.culprit, @r#""style-src http://cdn.example.com""#);
1480 }
1481
1482 #[test]
1483 fn test_csp_culprit_1() {
1484 let json = r#"{
1485 "csp-report": {
1486 "document-uri": "http://example.com/foo",
1487 "violated-directive": "style-src cdn.example.com",
1488 "effective-directive": "style-src"
1489 }
1490 }"#;
1491
1492 let mut event = Event::default();
1493 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1494 insta::assert_debug_snapshot!(event.culprit, @r#""style-src cdn.example.com""#);
1495 }
1496
1497 #[test]
1498 fn test_csp_culprit_2() {
1499 let json = r#"{
1500 "csp-report": {
1501 "document-uri": "https://example.com/foo",
1502 "violated-directive": "style-src cdn.example.com",
1503 "effective-directive": "style-src"
1504 }
1505 }"#;
1506
1507 let mut event = Event::default();
1508 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1509 insta::assert_debug_snapshot!(event.culprit, @r#""style-src cdn.example.com""#);
1510 }
1511
1512 #[test]
1513 fn test_csp_culprit_3() {
1514 let json = r#"{
1515 "csp-report": {
1516 "document-uri": "http://example.com/foo",
1517 "violated-directive": "style-src https://cdn.example.com",
1518 "effective-directive": "style-src"
1519 }
1520 }"#;
1521
1522 let mut event = Event::default();
1523 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1524 insta::assert_debug_snapshot!(event.culprit, @r#""style-src https://cdn.example.com""#);
1525 }
1526
1527 #[test]
1528 fn test_csp_culprit_4() {
1529 let json = r#"{
1530 "csp-report": {
1531 "document-uri": "http://example.com/foo",
1532 "violated-directive": "style-src http://example.com",
1533 "effective-directive": "style-src"
1534 }
1535 }"#;
1536
1537 let mut event = Event::default();
1538 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1539 insta::assert_debug_snapshot!(event.culprit, @r#""style-src 'self'""#);
1540 }
1541
1542 #[test]
1543 fn test_csp_culprit_5() {
1544 let json = r#"{
1545 "csp-report": {
1546 "document-uri": "http://example.com/foo",
1547 "violated-directive": "style-src http://example2.com example.com",
1548 "effective-directive": "style-src"
1549 }
1550 }"#;
1551
1552 let mut event = Event::default();
1553 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1554 insta::assert_debug_snapshot!(event.culprit, @r#""style-src http://example2.com 'self'""#);
1555 }
1556
1557 #[test]
1558 fn test_csp_culprit_uri_without_scheme() {
1559 let json = r#"{
1561 "csp-report": {
1562 "document-uri": "example.com",
1563 "violated-directive": "style-src example2.com"
1564 }
1565 }"#;
1566
1567 let mut event = Event::default();
1568 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1569 insta::assert_debug_snapshot!(event.culprit, @r#""style-src example2.com""#);
1570 }
1571
1572 #[test]
1573 fn test_csp_tags_stripe() {
1574 let json = r#"{
1578 "csp-report": {
1579 "document-uri": "https://example.com",
1580 "blocked-uri": "https://api.stripe.com/v1/tokens?card[number]=xxx",
1581 "effective-directive": "script-src"
1582 }
1583 }"#;
1584
1585 let mut event = Event::default();
1586 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1587 insta::assert_debug_snapshot!(event.tags, @r###"
1588 Tags(
1589 PairList(
1590 [
1591 TagEntry(
1592 "effective-directive",
1593 "script-src",
1594 ),
1595 TagEntry(
1596 "blocked-uri",
1597 "https://api.stripe.com/v1/tokens",
1598 ),
1599 TagEntry(
1600 "blocked-host",
1601 "api.stripe.com",
1602 ),
1603 ],
1604 ),
1605 )
1606 "###);
1607 }
1608
1609 #[test]
1610 fn test_csp_get_message_0() {
1611 let json = r#"{
1612 "csp-report": {
1613 "document-uri": "http://example.com/foo",
1614 "effective-directive": "img-src",
1615 "blocked-uri": "http://google.com/foo"
1616 }
1617 }"#;
1618
1619 let mut event = Event::default();
1620 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1621 let message = &event.logentry.value().unwrap().formatted;
1622 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'image' from 'google.com'""#);
1623 }
1624
1625 #[test]
1626 fn test_csp_get_message_1() {
1627 let json = r#"{
1628 "csp-report": {
1629 "document-uri": "http://example.com/foo",
1630 "effective-directive": "style-src",
1631 "blocked-uri": ""
1632 }
1633 }"#;
1634
1635 let mut event = Event::default();
1636 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1637 let message = &event.logentry.value().unwrap().formatted;
1638 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked inline 'style'""#);
1639 }
1640
1641 #[test]
1642 fn test_csp_get_message_2() {
1643 let json = r#"{
1644 "csp-report": {
1645 "document-uri": "http://example.com/foo",
1646 "effective-directive": "script-src",
1647 "blocked-uri": "",
1648 "violated-directive": "script-src 'unsafe-inline'"
1649 }
1650 }"#;
1651
1652 let mut event = Event::default();
1653 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1654 let message = &event.logentry.value().unwrap().formatted;
1655 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked unsafe inline 'script'""#);
1656 }
1657
1658 #[test]
1659 fn test_csp_get_message_3() {
1660 let json = r#"{
1661 "csp-report": {
1662 "document-uri": "http://example.com/foo",
1663 "effective-directive": "script-src",
1664 "blocked-uri": "",
1665 "violated-directive": "script-src 'unsafe-eval'"
1666 }
1667 }"#;
1668
1669 let mut event = Event::default();
1670 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1671 let message = &event.logentry.value().unwrap().formatted;
1672 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked unsafe eval() 'script'""#);
1673 }
1674
1675 #[test]
1676 fn test_csp_get_message_4() {
1677 let json = r#"{
1678 "csp-report": {
1679 "document-uri": "http://example.com/foo",
1680 "effective-directive": "script-src",
1681 "blocked-uri": "",
1682 "violated-directive": "script-src example.com"
1683 }
1684 }"#;
1685
1686 let mut event = Event::default();
1687 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1688 let message = &event.logentry.value().unwrap().formatted;
1689 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked unsafe (eval() or inline) 'script'""#);
1690 }
1691
1692 #[test]
1693 fn test_csp_get_message_5() {
1694 let json = r#"{
1695 "csp-report": {
1696 "document-uri": "http://example.com/foo",
1697 "effective-directive": "script-src",
1698 "blocked-uri": "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D"
1699 }
1700 }"#;
1701
1702 let mut event = Event::default();
1703 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1704 let message = &event.logentry.value().unwrap().formatted;
1705 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'script' from 'data:'""#);
1706 }
1707
1708 #[test]
1709 fn test_csp_get_message_6() {
1710 let json = r#"{
1711 "csp-report": {
1712 "document-uri": "http://example.com/foo",
1713 "effective-directive": "script-src",
1714 "blocked-uri": "data"
1715 }
1716 }"#;
1717
1718 let mut event = Event::default();
1719 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1720 let message = &event.logentry.value().unwrap().formatted;
1721 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'script' from 'data:'""#);
1722 }
1723
1724 #[test]
1725 fn test_csp_get_message_7() {
1726 let json = r#"{
1727 "csp-report": {
1728 "document-uri": "http://example.com/foo",
1729 "effective-directive": "style-src-elem",
1730 "blocked-uri": "http://fonts.google.com/foo"
1731 }
1732 }"#;
1733
1734 let mut event = Event::default();
1735 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1736 let message = &event.logentry.value().unwrap().formatted;
1737 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'style' from 'fonts.google.com'""#);
1738 }
1739
1740 #[test]
1741 fn test_csp_get_message_8() {
1742 let json = r#"{
1743 "csp-report": {
1744 "document-uri": "http://example.com/foo",
1745 "effective-directive": "script-src-elem",
1746 "blocked-uri": "http://cdn.ajaxapis.com/foo"
1747 }
1748 }"#;
1749
1750 let mut event = Event::default();
1751 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1752 let message = &event.logentry.value().unwrap().formatted;
1753 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'script' from 'cdn.ajaxapis.com'""#);
1754 }
1755
1756 #[test]
1757 fn test_csp_get_message_9() {
1758 let json = r#"{
1759 "csp-report": {
1760 "document-uri": "http://notlocalhost:8000/",
1761 "effective-directive": "style-src",
1762 "blocked-uri": "http://notlocalhost:8000/lol.css"
1763 }
1764 }"#;
1765
1766 let mut event = Event::default();
1767 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1768 let message = &event.logentry.value().unwrap().formatted;
1769 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'style' from 'notlocalhost:8000'""#);
1770 }
1771
1772 #[test]
1773 fn test_expectct_basic() {
1774 let json = r#"{
1775 "expect-ct-report": {
1776 "date-time": "2014-04-06T13:00:50Z",
1777 "hostname": "www.example.com",
1778 "port": 443,
1779 "scheme": "https",
1780 "effective-expiration-date": "2014-05-01T12:40:50Z",
1781 "served-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1782 "validated-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1783 "scts": [
1784 {
1785 "version": 1,
1786 "status": "invalid",
1787 "source": "embedded",
1788 "serialized_sct": "ABCD=="
1789 }
1790 ],
1791 "failure-mode": "enforce",
1792 "test-report": false
1793 }
1794 }"#;
1795
1796 let mut event = Event::default();
1797 ExpectCt::apply_to_event(json.as_bytes(), &mut event).unwrap();
1798 assert_annotated_snapshot!(Annotated::new(event), @r#"
1799 {
1800 "culprit": "www.example.com",
1801 "logentry": {
1802 "formatted": "Expect-CT failed for 'www.example.com'"
1803 },
1804 "request": {
1805 "url": "www.example.com"
1806 },
1807 "tags": [
1808 [
1809 "hostname",
1810 "www.example.com"
1811 ],
1812 [
1813 "port",
1814 "443"
1815 ]
1816 ],
1817 "expectct": {
1818 "date_time": "2014-04-06T13:00:50+00:00",
1819 "hostname": "www.example.com",
1820 "port": 443,
1821 "scheme": "https",
1822 "effective_expiration_date": "2014-05-01T12:40:50+00:00",
1823 "served_certificate_chain": [
1824 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1825 ],
1826 "validated_certificate_chain": [
1827 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1828 ],
1829 "scts": [
1830 {
1831 "version": 1,
1832 "status": "invalid",
1833 "source": "embedded",
1834 "serialized_sct": "ABCD=="
1835 }
1836 ],
1837 "failure_mode": "enforce",
1838 "test_report": false
1839 }
1840 }
1841 "#);
1842 }
1843
1844 #[test]
1845 fn test_expectct_invalid() {
1846 let json = r#"{
1847 "hostname": "www.example.com",
1848 "date_time": "Not an RFC3339 datetime"
1849 }"#;
1850
1851 let mut event = Event::default();
1852 ExpectCt::apply_to_event(json.as_bytes(), &mut event)
1853 .expect_err("date_time should fail to parse");
1854 }
1855
1856 #[test]
1857 fn test_expectstaple_basic() {
1858 let json = r#"{
1859 "expect-staple-report": {
1860 "date-time": "2014-04-06T13:00:50Z",
1861 "hostname": "www.example.com",
1862 "port": 443,
1863 "response-status": "ERROR_RESPONSE",
1864 "cert-status": "REVOKED",
1865 "effective-expiration-date": "2014-05-01T12:40:50Z",
1866 "served-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1867 "validated-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"]
1868 }
1869 }"#;
1870
1871 let mut event = Event::default();
1872 ExpectStaple::apply_to_event(json.as_bytes(), &mut event).unwrap();
1873 assert_annotated_snapshot!(Annotated::new(event), @r#"
1874 {
1875 "culprit": "www.example.com",
1876 "logentry": {
1877 "formatted": "Expect-Staple failed for 'www.example.com'"
1878 },
1879 "request": {
1880 "url": "www.example.com"
1881 },
1882 "tags": [
1883 [
1884 "hostname",
1885 "www.example.com"
1886 ],
1887 [
1888 "port",
1889 "443"
1890 ],
1891 [
1892 "response_status",
1893 "ERROR_RESPONSE"
1894 ],
1895 [
1896 "cert_status",
1897 "REVOKED"
1898 ]
1899 ],
1900 "expectstaple": {
1901 "date_time": "2014-04-06T13:00:50+00:00",
1902 "hostname": "www.example.com",
1903 "port": 443,
1904 "effective_expiration_date": "2014-05-01T12:40:50+00:00",
1905 "response_status": "ERROR_RESPONSE",
1906 "cert_status": "REVOKED",
1907 "served_certificate_chain": [
1908 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1909 ],
1910 "validated_certificate_chain": [
1911 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1912 ]
1913 }
1914 }
1915 "#);
1916 }
1917
1918 #[test]
1919 fn test_hpkp_basic() {
1920 let json = r#"{
1921 "date-time": "2014-04-06T13:00:50Z",
1922 "hostname": "example.com",
1923 "port": 443,
1924 "effective-expiration-date": "2014-05-01T12:40:50Z",
1925 "include-subdomains": false,
1926 "served-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1927 "validated-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1928 "known-pins": ["pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\""]
1929 }"#;
1930
1931 let mut event = Event::default();
1932 Hpkp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1933 assert_annotated_snapshot!(Annotated::new(event), @r#"
1934 {
1935 "logentry": {
1936 "formatted": "Public key pinning validation failed for 'example.com'"
1937 },
1938 "request": {
1939 "url": "example.com"
1940 },
1941 "tags": [
1942 [
1943 "hostname",
1944 "example.com"
1945 ],
1946 [
1947 "port",
1948 "443"
1949 ],
1950 [
1951 "include-subdomains",
1952 "false"
1953 ]
1954 ],
1955 "hpkp": {
1956 "date_time": "2014-04-06T13:00:50+00:00",
1957 "hostname": "example.com",
1958 "port": 443,
1959 "effective_expiration_date": "2014-05-01T12:40:50+00:00",
1960 "include_subdomains": false,
1961 "served_certificate_chain": [
1962 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1963 ],
1964 "validated_certificate_chain": [
1965 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1966 ],
1967 "known_pins": [
1968 "pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\""
1969 ]
1970 }
1971 }
1972 "#);
1973 }
1974
1975 #[test]
1976 fn test_security_report_type_deserializer_recognizes_csp_reports() {
1977 let csp_report_text = r#"{
1978 "csp-report": {
1979 "document-uri": "https://example.com/foo/bar",
1980 "referrer": "https://www.google.com/",
1981 "violated-directive": "default-src self",
1982 "original-policy": "default-src self; report-uri /csp-hotline.php",
1983 "blocked-uri": "http://evilhackerscripts.com"
1984 }
1985 }"#;
1986
1987 let report_type = SecurityReportType::from_json(csp_report_text.as_bytes()).unwrap();
1988 assert_eq!(report_type, Some(SecurityReportType::Csp));
1989 }
1990
1991 #[test]
1992 fn test_security_report_type_deserializer_recognizes_csp_violations_reports() {
1993 let csp_report_text = r#"{
1994 "age":0,
1995 "body":{
1996 "blockedURL":"https://example.com/tst/media/7_del.png",
1997 "disposition":"enforce",
1998 "documentURL":"https://example.com/tst/test_frame.php?ID=229&hash=da964209653e467d337313e51876e27d",
1999 "effectiveDirective":"img-src",
2000 "lineNumber":9,
2001 "originalPolicy":"default-src 'none'; report-to endpoint-csp;",
2002 "referrer":"https://example.com/test229/",
2003 "sourceFile":"https://example.com/tst/test_frame.php?ID=229&hash=da964209653e467d337313e51876e27d",
2004 "statusCode":0
2005 },
2006 "type":"csp-violation",
2007 "url":"https://example.com/tst/test_frame.php?ID=229&hash=da964209653e467d337313e51876e27d",
2008 "user_agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
2009 }"#;
2010
2011 let report_type = SecurityReportType::from_json(csp_report_text.as_bytes()).unwrap();
2012 assert_eq!(report_type, Some(SecurityReportType::Csp));
2013 }
2014
2015 #[test]
2016 fn test_security_report_type_deserializer_recognizes_expect_ct_reports() {
2017 let expect_ct_report_text = r#"{
2018 "expect-ct-report": {
2019 "date-time": "2014-04-06T13:00:50Z",
2020 "hostname": "www.example.com",
2021 "port": 443,
2022 "effective-expiration-date": "2014-05-01T12:40:50Z",
2023 "served-certificate-chain": [
2024 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
2025 ],
2026 "validated-certificate-chain": [
2027 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
2028 ],
2029 "scts": [
2030 {
2031 "version": 1,
2032 "status": "invalid",
2033 "source": "embedded",
2034 "serialized_sct": "ABCD=="
2035 }
2036 ]
2037 }
2038 }"#;
2039
2040 let report_type = SecurityReportType::from_json(expect_ct_report_text.as_bytes()).unwrap();
2041 assert_eq!(report_type, Some(SecurityReportType::ExpectCt));
2042 }
2043
2044 #[test]
2045 fn test_security_report_type_deserializer_recognizes_expect_staple_reports() {
2046 let expect_staple_report_text = r#"{
2047 "expect-staple-report": {
2048 "date-time": "2014-04-06T13:00:50Z",
2049 "hostname": "www.example.com",
2050 "port": 443,
2051 "response-status": "ERROR_RESPONSE",
2052 "cert-status": "REVOKED",
2053 "effective-expiration-date": "2014-05-01T12:40:50Z",
2054 "served-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
2055 "validated-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"]
2056 }
2057 }"#;
2058 let report_type =
2059 SecurityReportType::from_json(expect_staple_report_text.as_bytes()).unwrap();
2060 assert_eq!(report_type, Some(SecurityReportType::ExpectStaple));
2061 }
2062
2063 #[test]
2064 fn test_security_report_type_deserializer_recognizes_hpkp_reports() {
2065 let hpkp_report_text = r#"{
2066 "date-time": "2014-04-06T13:00:50Z",
2067 "hostname": "www.example.com",
2068 "port": 443,
2069 "effective-expiration-date": "2014-05-01T12:40:50Z",
2070 "include-subdomains": false,
2071 "served-certificate-chain": [
2072 "-----BEGIN CERTIFICATE-----\n MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\n... -----END CERTIFICATE-----"
2073 ],
2074 "validated-certificate-chain": [
2075 "-----BEGIN CERTIFICATE-----\n MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\n... -----END CERTIFICATE-----"
2076 ],
2077 "known-pins": [
2078 "pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"",
2079 "pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\""
2080 ]
2081 }"#;
2082
2083 let report_type = SecurityReportType::from_json(hpkp_report_text.as_bytes()).unwrap();
2084 assert_eq!(report_type, Some(SecurityReportType::Hpkp));
2085 }
2086
2087 #[test]
2088 fn test_effective_directive_from_violated_directive_single() {
2089 let csp_raw: CspRaw =
2091 serde_json::from_str(r#"{"violated-directive":"default-src"}"#).unwrap();
2092 assert!(matches!(
2093 csp_raw.effective_directive(),
2094 Ok(CspDirective::DefaultSrc)
2095 ));
2096 }
2097
2098 #[test]
2099 fn test_extract_effective_directive_from_long_form() {
2100 let json = r#"{
2102 "csp-report": {
2103 "document-uri": "http://example.com/foo",
2104 "effective-directive": "script-src 'report-sample' 'strict-dynamic' 'unsafe-eval' 'nonce-random" ,
2105 "blocked-uri": "data"
2106 }
2107 }"#;
2108
2109 let raw_report = serde_json::from_slice::<CspReportRaw>(json.as_bytes()).unwrap();
2110 let raw_csp = raw_report.csp_report;
2111
2112 let effective_directive = raw_csp.effective_directive().unwrap();
2113
2114 assert_eq!(effective_directive, CspDirective::ScriptSrc);
2115
2116 let json = r#"{
2118 "csp-report": {
2119 "document-uri": "http://example.com/foo",
2120 "violated-directive": "script-src 'report-sample' 'strict-dynamic' 'unsafe-eval' 'nonce-random" ,
2121 "blocked-uri": "data"
2122 }
2123 }"#;
2124
2125 let raw_report = serde_json::from_slice::<CspReportRaw>(json.as_bytes()).unwrap();
2126 let raw_csp = raw_report.csp_report;
2127
2128 let effective_directive = raw_csp.effective_directive().unwrap();
2129
2130 assert_eq!(effective_directive, CspDirective::ScriptSrc);
2131 }
2132
2133 #[test]
2134 fn test_csp_report_fenced_frame_src() {
2135 let json = r#"
2136 {
2137 "csp-report": {
2138 "document-uri": "https://example.com",
2139 "referrer": "https://www.google.com/",
2140 "violated-directive": "fenced-frame-src",
2141 "effective-directive": "fenced-frame-src",
2142 "original-policy": "default-src 'self' 'unsafe-eval'",
2143 "disposition": "enforce",
2144 "blocked-uri": "",
2145 "status-code": 200,
2146 "script-sample": ""
2147 }
2148 }
2149 "#;
2150 let raw_report = serde_json::from_slice::<CspReportRaw>(json.as_bytes()).unwrap();
2151 let raw_csp = raw_report.csp_report;
2152
2153 assert!(raw_csp.effective_directive().is_ok());
2154 }
2155}