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