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_owned()
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_owned(),
319 CspDirective::ConnectSrc => "Blocked inline 'connect'".to_owned(),
320 CspDirective::FontSrc => "Blocked inline 'font'".to_owned(),
321 CspDirective::ImgSrc => "Blocked inline 'image'".to_owned(),
322 CspDirective::ManifestSrc => "Blocked inline 'manifest'".to_owned(),
323 CspDirective::MediaSrc => "Blocked inline 'media'".to_owned(),
324 CspDirective::ObjectSrc => "Blocked inline 'object'".to_owned(),
325 CspDirective::ScriptSrcAttr => "Blocked unsafe 'script' element".to_owned(),
326 CspDirective::ScriptSrcElem => "Blocked inline script attribute".to_owned(),
327 CspDirective::StyleSrc => "Blocked inline 'style'".to_owned(),
328 CspDirective::StyleSrcElem => "Blocked 'style' or 'link' element".to_owned(),
329 CspDirective::StyleSrcAttr => "Blocked style attribute".to_owned(),
330 CspDirective::ScriptSrc => {
331 if self.violated_directive.contains("'unsafe-inline'") {
332 "Blocked unsafe inline 'script'".to_owned()
333 } else if self.violated_directive.contains("'unsafe-eval'") {
334 "Blocked unsafe eval() 'script'".to_owned()
335 } else {
336 "Blocked unsafe (eval() or inline) 'script'".to_owned()
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_owned()),
471 Annotated::new(effective_directive.to_string()),
472 )),
473 Annotated::new(TagEntry(
474 Annotated::new("blocked-uri".to_owned()),
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_owned()),
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 #[metastructure(pii = "maybe")]
577 pub source_file: Annotated<String>,
578 pub line_number: Annotated<u64>,
580 pub column_number: Annotated<u64>,
582 pub script_sample: Annotated<String>,
585 pub disposition: Annotated<String>,
587 #[metastructure(pii = "true", additional_properties)]
589 pub other: Object<Value>,
590}
591
592impl Csp {
593 pub fn apply_to_event(data: &[u8], event: &mut Event) -> Result<(), serde_json::Error> {
594 let variant = serde_json::from_slice::<CspVariant>(data)?;
595 match variant {
596 CspVariant::Csp { csp_report } => Csp::extract_report(event, csp_report)?,
597 CspVariant::CspViolation { body } => Csp::extract_report(event, body)?,
598 }
599
600 Ok(())
601 }
602
603 fn extract_report(event: &mut Event, raw_csp: CspRaw) -> Result<(), serde_json::Error> {
604 let effective_directive = raw_csp
605 .effective_directive()
606 .map_err(serde::de::Error::custom)?;
607
608 event.logentry = Annotated::new(LogEntry::from(raw_csp.get_message(effective_directive)));
609 event.culprit = Annotated::new(raw_csp.get_culprit());
610 event.tags = Annotated::new(raw_csp.get_tags(effective_directive));
611 event.request = Annotated::new(raw_csp.get_request());
612 event.csp = Annotated::new(raw_csp.into_protocol(effective_directive));
613
614 Ok(())
615 }
616}
617
618#[derive(Clone, Copy, Debug, PartialEq, Eq)]
619enum ExpectCtStatus {
620 Unknown,
621 Valid,
622 Invalid,
623}
624
625relay_common::derive_fromstr_and_display!(ExpectCtStatus, InvalidSecurityError, {
626 ExpectCtStatus::Unknown => "unknown",
627 ExpectCtStatus::Valid => "valid",
628 ExpectCtStatus::Invalid => "invalid",
629});
630
631relay_common::impl_str_serde!(ExpectCtStatus, "an expect-ct status");
632
633#[derive(Clone, Copy, Debug, PartialEq, Eq)]
634enum ExpectCtSource {
635 TlsExtension,
636 Ocsp,
637 Embedded,
638}
639
640relay_common::derive_fromstr_and_display!(ExpectCtSource, InvalidSecurityError, {
641 ExpectCtSource::TlsExtension => "tls-extension",
642 ExpectCtSource::Ocsp => "ocsp",
643 ExpectCtSource::Embedded => "embedded",
644});
645
646relay_common::impl_str_serde!(ExpectCtSource, "an expect-ct source");
647
648#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
649struct SingleCertificateTimestampRaw {
650 version: Option<i64>,
651 status: Option<ExpectCtStatus>,
652 source: Option<ExpectCtSource>,
653 serialized_sct: Option<String>, }
655
656impl SingleCertificateTimestampRaw {
657 fn into_protocol(self) -> SingleCertificateTimestamp {
658 SingleCertificateTimestamp {
659 version: Annotated::from(self.version),
660 status: Annotated::from(self.status.map(|s| s.to_string())),
661 source: Annotated::from(self.source.map(|s| s.to_string())),
662 serialized_sct: Annotated::from(self.serialized_sct),
663 }
664 }
665}
666
667#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
668#[serde(rename_all = "kebab-case")]
669struct ExpectCtRaw {
670 #[serde(with = "serde_date_time_3339")]
671 date_time: Option<DateTime<Utc>>,
672 hostname: String,
673 port: Option<i64>,
674 scheme: Option<String>,
675 #[serde(with = "serde_date_time_3339")]
676 effective_expiration_date: Option<DateTime<Utc>>,
677 served_certificate_chain: Option<Vec<String>>,
678 validated_certificate_chain: Option<Vec<String>>,
679 scts: Option<Vec<SingleCertificateTimestampRaw>>,
680 failure_mode: Option<String>,
681 test_report: Option<bool>,
682}
683
684mod serde_date_time_3339 {
685 use serde::de::Visitor;
686
687 use super::*;
688
689 pub fn serialize<S>(date_time: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
690 where
691 S: serde::Serializer,
692 {
693 match date_time {
694 None => serializer.serialize_none(),
695 Some(d) => serializer.serialize_str(&d.to_rfc3339()),
696 }
697 }
698
699 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
700 where
701 D: serde::Deserializer<'de>,
702 {
703 struct DateTimeVisitor;
704
705 impl Visitor<'_> for DateTimeVisitor {
706 type Value = Option<DateTime<Utc>>;
707
708 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
709 formatter.write_str("expected a date-time in RFC 3339 format")
710 }
711
712 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
713 where
714 E: serde::de::Error,
715 {
716 DateTime::parse_from_rfc3339(s)
717 .map(|d| Some(d.with_timezone(&Utc)))
718 .map_err(serde::de::Error::custom)
719 }
720
721 fn visit_none<E>(self) -> Result<Self::Value, E>
722 where
723 E: Error,
724 {
725 Ok(None)
726 }
727 }
728
729 deserializer.deserialize_any(DateTimeVisitor)
730 }
731}
732
733impl ExpectCtRaw {
734 fn get_message(&self) -> String {
735 format!("Expect-CT failed for '{}'", self.hostname)
736 }
737
738 fn into_protocol(self) -> ExpectCt {
739 ExpectCt {
740 date_time: Annotated::from(self.date_time.map(|d| d.to_rfc3339())),
741 hostname: Annotated::from(self.hostname),
742 port: Annotated::from(self.port),
743 scheme: Annotated::from(self.scheme),
744 effective_expiration_date: Annotated::from(
745 self.effective_expiration_date.map(|d| d.to_rfc3339()),
746 ),
747 served_certificate_chain: Annotated::new(
748 self.served_certificate_chain
749 .map(|s| s.into_iter().map(Annotated::from).collect())
750 .unwrap_or_default(),
751 ),
752
753 validated_certificate_chain: Annotated::new(
754 self.validated_certificate_chain
755 .map(|v| v.into_iter().map(Annotated::from).collect())
756 .unwrap_or_default(),
757 ),
758 scts: Annotated::from(self.scts.map(|scts| {
759 scts.into_iter()
760 .map(|elm| Annotated::from(elm.into_protocol()))
761 .collect()
762 })),
763 failure_mode: Annotated::from(self.failure_mode),
764 test_report: Annotated::from(self.test_report),
765 }
766 }
767
768 fn get_culprit(&self) -> String {
769 self.hostname.clone()
770 }
771
772 fn get_tags(&self) -> Tags {
773 let mut tags = vec![Annotated::new(TagEntry(
774 Annotated::new("hostname".to_owned()),
775 Annotated::new(self.hostname.clone()),
776 ))];
777
778 if let Some(port) = self.port {
779 tags.push(Annotated::new(TagEntry(
780 Annotated::new("port".to_owned()),
781 Annotated::new(port.to_string()),
782 )));
783 }
784
785 Tags(PairList::from(tags))
786 }
787
788 fn get_request(&self) -> Request {
789 Request {
790 url: Annotated::from(self.hostname.clone()),
791 ..Request::default()
792 }
793 }
794}
795
796#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
797#[serde(rename_all = "kebab-case")]
798struct ExpectCtReportRaw {
799 expect_ct_report: ExpectCtRaw,
800}
801
802#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
806pub struct SingleCertificateTimestamp {
807 pub version: Annotated<i64>,
808 pub status: Annotated<String>,
809 pub source: Annotated<String>,
810 pub serialized_sct: Annotated<String>,
811}
812
813#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
817pub struct ExpectCt {
818 pub date_time: Annotated<String>,
821 pub hostname: Annotated<String>,
823 pub port: Annotated<i64>,
824 pub scheme: Annotated<String>,
825 pub effective_expiration_date: Annotated<String>,
827 pub served_certificate_chain: Annotated<Array<String>>,
828 pub validated_certificate_chain: Annotated<Array<String>>,
829 pub scts: Annotated<Array<SingleCertificateTimestamp>>,
830 pub failure_mode: Annotated<String>,
831 pub test_report: Annotated<bool>,
832}
833
834impl ExpectCt {
835 pub fn apply_to_event(data: &[u8], event: &mut Event) -> Result<(), serde_json::Error> {
836 let raw_report = serde_json::from_slice::<ExpectCtReportRaw>(data)?;
837 let raw_expect_ct = raw_report.expect_ct_report;
838
839 event.logentry = Annotated::new(LogEntry::from(raw_expect_ct.get_message()));
840 event.culprit = Annotated::new(raw_expect_ct.get_culprit());
841 event.tags = Annotated::new(raw_expect_ct.get_tags());
842 event.request = Annotated::new(raw_expect_ct.get_request());
843 event.expectct = Annotated::new(raw_expect_ct.into_protocol());
844
845 Ok(())
846 }
847}
848
849#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
853#[serde(rename_all = "kebab-case")]
854struct HpkpRaw {
855 #[serde(skip_serializing_if = "Option::is_none")]
856 date_time: Option<DateTime<Utc>>,
857 hostname: String,
858 #[serde(skip_serializing_if = "Option::is_none")]
859 port: Option<u64>,
860 #[serde(skip_serializing_if = "Option::is_none")]
861 effective_expiration_date: Option<DateTime<Utc>>,
862 #[serde(skip_serializing_if = "Option::is_none")]
863 include_subdomains: Option<bool>,
864 #[serde(skip_serializing_if = "Option::is_none")]
865 noted_hostname: Option<String>,
866 #[serde(skip_serializing_if = "Option::is_none")]
867 served_certificate_chain: Option<Vec<String>>,
868 #[serde(skip_serializing_if = "Option::is_none")]
869 validated_certificate_chain: Option<Vec<String>>,
870 known_pins: Vec<String>,
871 #[serde(flatten)]
872 other: BTreeMap<String, serde_json::Value>,
873}
874
875impl HpkpRaw {
876 fn get_message(&self) -> String {
877 format!(
878 "Public key pinning validation failed for '{}'",
879 self.hostname
880 )
881 }
882
883 fn into_protocol(self) -> Hpkp {
884 Hpkp {
885 date_time: Annotated::from(self.date_time.map(|d| d.to_rfc3339())),
886 hostname: Annotated::new(self.hostname),
887 port: Annotated::from(self.port),
888 effective_expiration_date: Annotated::from(
889 self.effective_expiration_date.map(|d| d.to_rfc3339()),
890 ),
891 include_subdomains: Annotated::from(self.include_subdomains),
892 noted_hostname: Annotated::from(self.noted_hostname),
893 served_certificate_chain: Annotated::from(
894 self.served_certificate_chain
895 .map(|chain| chain.into_iter().map(Annotated::from).collect()),
896 ),
897 validated_certificate_chain: Annotated::from(
898 self.validated_certificate_chain
899 .map(|chain| chain.into_iter().map(Annotated::from).collect()),
900 ),
901 known_pins: Annotated::new(self.known_pins.into_iter().map(Annotated::from).collect()),
902 other: self
903 .other
904 .into_iter()
905 .map(|(k, v)| (k, Annotated::from(v)))
906 .collect(),
907 }
908 }
909
910 fn get_tags(&self) -> Tags {
911 let mut tags = vec![Annotated::new(TagEntry(
912 Annotated::new("hostname".to_owned()),
913 Annotated::new(self.hostname.clone()),
914 ))];
915
916 if let Some(port) = self.port {
917 tags.push(Annotated::new(TagEntry(
918 Annotated::new("port".to_owned()),
919 Annotated::new(port.to_string()),
920 )));
921 }
922
923 if let Some(include_subdomains) = self.include_subdomains {
924 tags.push(Annotated::new(TagEntry(
925 Annotated::new("include-subdomains".to_owned()),
926 Annotated::new(include_subdomains.to_string()),
927 )));
928 }
929
930 Tags(PairList::from(tags))
931 }
932
933 fn get_request(&self) -> Request {
934 Request {
935 url: Annotated::from(self.hostname.clone()),
936 ..Request::default()
937 }
938 }
939}
940
941#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
943pub struct Hpkp {
944 pub date_time: Annotated<String>,
946 pub hostname: Annotated<String>,
948 pub port: Annotated<u64>,
950 pub effective_expiration_date: Annotated<String>,
952 pub include_subdomains: Annotated<bool>,
955
956 pub noted_hostname: Annotated<String>,
960 pub served_certificate_chain: Annotated<Array<String>>,
966 pub validated_certificate_chain: Annotated<Array<String>>,
968
969 #[metastructure(required = true)]
972 pub known_pins: Annotated<Array<String>>,
973
974 #[metastructure(pii = "true", additional_properties)]
975 pub other: Object<Value>,
976}
977
978impl Hpkp {
979 pub fn apply_to_event(data: &[u8], event: &mut Event) -> Result<(), serde_json::Error> {
980 let raw_hpkp = serde_json::from_slice::<HpkpRaw>(data)?;
981
982 event.logentry = Annotated::new(LogEntry::from(raw_hpkp.get_message()));
983 event.tags = Annotated::new(raw_hpkp.get_tags());
984 event.request = Annotated::new(raw_hpkp.get_request());
985 event.hpkp = Annotated::new(raw_hpkp.into_protocol());
986
987 Ok(())
988 }
989}
990
991#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
995#[serde(rename_all = "kebab-case")]
996struct ExpectStapleReportRaw {
997 expect_staple_report: ExpectStapleRaw,
998}
999
1000#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1001pub enum ExpectStapleResponseStatus {
1002 Missing,
1003 Provided,
1004 ErrorResponse,
1005 BadProducedAt,
1006 NoMatchingResponse,
1007 InvalidDate,
1008 ParseResponseError,
1009 ParseResponseDataError,
1010}
1011
1012relay_common::derive_fromstr_and_display!(ExpectStapleResponseStatus, InvalidSecurityError, {
1013 ExpectStapleResponseStatus::Missing => "MISSING",
1014 ExpectStapleResponseStatus::Provided => "PROVIDED",
1015 ExpectStapleResponseStatus::ErrorResponse => "ERROR_RESPONSE",
1016 ExpectStapleResponseStatus::BadProducedAt => "BAD_PRODUCED_AT",
1017 ExpectStapleResponseStatus::NoMatchingResponse => "NO_MATCHING_RESPONSE",
1018 ExpectStapleResponseStatus::InvalidDate => "INVALID_DATE",
1019 ExpectStapleResponseStatus::ParseResponseError => "PARSE_RESPONSE_ERROR",
1020 ExpectStapleResponseStatus::ParseResponseDataError => "PARSE_RESPONSE_DATA_ERROR",
1021});
1022
1023relay_common::impl_str_serde!(ExpectStapleResponseStatus, "an expect-ct response status");
1024
1025#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1026pub enum ExpectStapleCertStatus {
1027 Good,
1028 Revoked,
1029 Unknown,
1030}
1031
1032relay_common::derive_fromstr_and_display!(ExpectStapleCertStatus, InvalidSecurityError, {
1033 ExpectStapleCertStatus::Good => "GOOD",
1034 ExpectStapleCertStatus::Revoked => "REVOKED",
1035 ExpectStapleCertStatus::Unknown => "UNKNOWN",
1036});
1037
1038relay_common::impl_str_serde!(ExpectStapleCertStatus, "an expect-staple cert status");
1039
1040#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
1042#[serde(rename_all = "kebab-case")]
1043struct ExpectStapleRaw {
1044 #[serde(skip_serializing_if = "Option::is_none")]
1045 date_time: Option<DateTime<Utc>>,
1046 hostname: String,
1047 #[serde(skip_serializing_if = "Option::is_none")]
1048 port: Option<i64>,
1049 #[serde(skip_serializing_if = "Option::is_none")]
1050 effective_expiration_date: Option<DateTime<Utc>>,
1051 #[serde(skip_serializing_if = "Option::is_none")]
1052 response_status: Option<ExpectStapleResponseStatus>,
1053 #[serde(skip_serializing_if = "Option::is_none")]
1054 ocsp_response: Option<Value>,
1055 #[serde(skip_serializing_if = "Option::is_none")]
1056 cert_status: Option<ExpectStapleCertStatus>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1058 served_certificate_chain: Option<Vec<String>>,
1059 #[serde(skip_serializing_if = "Option::is_none")]
1060 validated_certificate_chain: Option<Vec<String>>,
1061}
1062
1063impl ExpectStapleRaw {
1064 fn get_message(&self) -> String {
1065 format!("Expect-Staple failed for '{}'", self.hostname)
1066 }
1067
1068 fn into_protocol(self) -> ExpectStaple {
1069 ExpectStaple {
1070 date_time: Annotated::from(self.date_time.map(|d| d.to_rfc3339())),
1071 hostname: Annotated::from(self.hostname),
1072 port: Annotated::from(self.port),
1073 effective_expiration_date: Annotated::from(
1074 self.effective_expiration_date.map(|d| d.to_rfc3339()),
1075 ),
1076 response_status: Annotated::from(self.response_status.map(|rs| rs.to_string())),
1077 cert_status: Annotated::from(self.cert_status.map(|cs| cs.to_string())),
1078 served_certificate_chain: Annotated::from(
1079 self.served_certificate_chain
1080 .map(|cert_chain| cert_chain.into_iter().map(Annotated::from).collect()),
1081 ),
1082 validated_certificate_chain: Annotated::from(
1083 self.validated_certificate_chain
1084 .map(|cert_chain| cert_chain.into_iter().map(Annotated::from).collect()),
1085 ),
1086 ocsp_response: Annotated::from(self.ocsp_response),
1087 }
1088 }
1089
1090 fn get_culprit(&self) -> String {
1091 self.hostname.clone()
1092 }
1093
1094 fn get_tags(&self) -> Tags {
1095 let mut tags = vec![Annotated::new(TagEntry(
1096 Annotated::new("hostname".to_owned()),
1097 Annotated::new(self.hostname.clone()),
1098 ))];
1099
1100 if let Some(port) = self.port {
1101 tags.push(Annotated::new(TagEntry(
1102 Annotated::new("port".to_owned()),
1103 Annotated::new(port.to_string()),
1104 )));
1105 }
1106
1107 if let Some(response_status) = self.response_status {
1108 tags.push(Annotated::new(TagEntry(
1109 Annotated::new("response_status".to_owned()),
1110 Annotated::new(response_status.to_string()),
1111 )));
1112 }
1113
1114 if let Some(cert_status) = self.cert_status {
1115 tags.push(Annotated::new(TagEntry(
1116 Annotated::new("cert_status".to_owned()),
1117 Annotated::new(cert_status.to_string()),
1118 )));
1119 }
1120
1121 Tags(PairList::from(tags))
1122 }
1123
1124 fn get_request(&self) -> Request {
1125 Request {
1126 url: Annotated::from(self.hostname.clone()),
1127 ..Request::default()
1128 }
1129 }
1130}
1131
1132#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
1136pub struct ExpectStaple {
1137 date_time: Annotated<String>,
1138 hostname: Annotated<String>,
1139 port: Annotated<i64>,
1140 effective_expiration_date: Annotated<String>,
1141 response_status: Annotated<String>,
1142 cert_status: Annotated<String>,
1143 served_certificate_chain: Annotated<Array<String>>,
1144 validated_certificate_chain: Annotated<Array<String>>,
1145 ocsp_response: Annotated<Value>,
1146}
1147
1148impl ExpectStaple {
1149 pub fn apply_to_event(data: &[u8], event: &mut Event) -> Result<(), serde_json::Error> {
1150 let raw_report = serde_json::from_slice::<ExpectStapleReportRaw>(data)?;
1151 let raw_expect_staple = raw_report.expect_staple_report;
1152
1153 event.logentry = Annotated::new(LogEntry::from(raw_expect_staple.get_message()));
1154 event.culprit = Annotated::new(raw_expect_staple.get_culprit());
1155 event.tags = Annotated::new(raw_expect_staple.get_tags());
1156 event.request = Annotated::new(raw_expect_staple.get_request());
1157 event.expectstaple = Annotated::new(raw_expect_staple.into_protocol());
1158
1159 Ok(())
1160 }
1161}
1162
1163#[derive(Clone, Debug, PartialEq, Eq)]
1164pub enum SecurityReportType {
1165 Csp,
1166 ExpectCt,
1167 ExpectStaple,
1168 Hpkp,
1169 Unsupported,
1170}
1171
1172impl SecurityReportType {
1173 pub fn from_json(data: &[u8]) -> Result<Option<Self>, serde_json::Error> {
1178 #[derive(Deserialize)]
1179 #[serde(rename_all = "kebab-case")]
1180 struct SecurityReport {
1181 #[serde(rename = "type")]
1182 ty: Option<CspViolationType>,
1183 csp_report: Option<IgnoredAny>,
1184 known_pins: Option<IgnoredAny>,
1185 expect_staple_report: Option<IgnoredAny>,
1186 expect_ct_report: Option<IgnoredAny>,
1187 }
1188
1189 let helper: SecurityReport = serde_json::from_slice(data)?;
1190
1191 Ok(if helper.csp_report.is_some() {
1192 Some(SecurityReportType::Csp)
1193 } else if let Some(CspViolationType::CspViolation) = helper.ty {
1194 Some(SecurityReportType::Csp)
1195 } else if let Some(CspViolationType::Other) = helper.ty {
1196 Some(SecurityReportType::Unsupported)
1197 } else if helper.known_pins.is_some() {
1198 Some(SecurityReportType::Hpkp)
1199 } else if helper.expect_staple_report.is_some() {
1200 Some(SecurityReportType::ExpectStaple)
1201 } else if helper.expect_ct_report.is_some() {
1202 Some(SecurityReportType::ExpectCt)
1203 } else {
1204 None
1205 })
1206 }
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211 use super::*;
1212 use relay_protocol::assert_annotated_snapshot;
1213
1214 #[test]
1215 fn test_unsplit_uri() {
1216 assert_eq!(unsplit_uri("", ""), "");
1217 assert_eq!(unsplit_uri("data", ""), "data:");
1218 assert_eq!(unsplit_uri("data", "foo"), "data://foo");
1219 assert_eq!(unsplit_uri("http", ""), "http://");
1220 assert_eq!(unsplit_uri("http", "foo"), "http://foo");
1221 }
1222
1223 #[test]
1224 fn test_normalize_uri() {
1225 assert_eq!(normalize_uri(""), "'self'");
1227 assert_eq!(normalize_uri("self"), "'self'");
1228
1229 assert_eq!(normalize_uri("data"), "data:");
1231 assert_eq!(normalize_uri("http"), "http://");
1232
1233 assert_eq!(normalize_uri("http://notlocalhost/"), "notlocalhost");
1235 assert_eq!(normalize_uri("https://notlocalhost/"), "notlocalhost");
1236 assert_eq!(normalize_uri("data://notlocalhost/"), "data://notlocalhost");
1237 assert_eq!(normalize_uri("http://notlocalhost/lol.css"), "notlocalhost");
1238
1239 assert_eq!(
1241 normalize_uri("http://notlocalhost:8000/"),
1242 "notlocalhost:8000"
1243 );
1244 assert_eq!(
1245 normalize_uri("http://notlocalhost:8000/lol.css"),
1246 "notlocalhost:8000"
1247 );
1248
1249 assert_eq!(normalize_uri("xyz://notlocalhost/"), "xyz://notlocalhost");
1251 }
1252
1253 #[test]
1254 fn test_csp_basic() {
1255 let json = r#"{
1256 "csp-report": {
1257 "document-uri": "http://example.com",
1258 "violated-directive": "style-src cdn.example.com",
1259 "blocked-uri": "http://example.com/lol.css",
1260 "effective-directive": "style-src",
1261 "status-code": "200"
1262 }
1263 }"#;
1264
1265 let mut event = Event::default();
1266 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1267
1268 assert_annotated_snapshot!(Annotated::new(event), @r###"
1269 {
1270 "culprit": "style-src cdn.example.com",
1271 "logentry": {
1272 "formatted": "Blocked 'style' from 'example.com'"
1273 },
1274 "request": {
1275 "url": "http://example.com"
1276 },
1277 "tags": [
1278 [
1279 "effective-directive",
1280 "style-src"
1281 ],
1282 [
1283 "blocked-uri",
1284 "http://example.com/lol.css"
1285 ],
1286 [
1287 "blocked-host",
1288 "example.com"
1289 ]
1290 ],
1291 "csp": {
1292 "effective_directive": "style-src",
1293 "blocked_uri": "http://example.com/lol.css",
1294 "document_uri": "http://example.com",
1295 "status_code": 200,
1296 "violated_directive": "style-src cdn.example.com"
1297 }
1298 }
1299 "###);
1300 }
1301
1302 #[test]
1303 fn test_csp_coerce_blocked_uri_if_missing() {
1304 let json = r#"{
1305 "csp-report": {
1306 "document-uri": "http://example.com",
1307 "effective-directive": "script-src"
1308 }
1309 }"#;
1310
1311 let mut event = Event::default();
1312 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1313
1314 assert_annotated_snapshot!(Annotated::new(event), @r#"
1315 {
1316 "culprit": "",
1317 "logentry": {
1318 "formatted": "Blocked unsafe (eval() or inline) 'script'"
1319 },
1320 "request": {
1321 "url": "http://example.com"
1322 },
1323 "tags": [
1324 [
1325 "effective-directive",
1326 "script-src"
1327 ],
1328 [
1329 "blocked-uri",
1330 "self"
1331 ]
1332 ],
1333 "csp": {
1334 "effective_directive": "script-src",
1335 "blocked_uri": "self",
1336 "document_uri": "http://example.com",
1337 "violated_directive": ""
1338 }
1339 }
1340 "#);
1341 }
1342
1343 #[test]
1344 fn test_csp_msdn() {
1345 let json = r#"{
1346 "csp-report": {
1347 "document-uri": "https://example.com/foo/bar",
1348 "referrer": "https://www.google.com/",
1349 "violated-directive": "default-src self",
1350 "original-policy": "default-src self; report-uri /csp-hotline.php",
1351 "blocked-uri": "http://evilhackerscripts.com"
1352 }
1353 }"#;
1354
1355 let mut event = Event::default();
1356 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1357
1358 assert_annotated_snapshot!(Annotated::new(event), @r###"
1359 {
1360 "culprit": "default-src self",
1361 "logentry": {
1362 "formatted": "Blocked 'default-src' from 'evilhackerscripts.com'"
1363 },
1364 "request": {
1365 "url": "https://example.com/foo/bar",
1366 "headers": [
1367 [
1368 "Referer",
1369 "https://www.google.com/"
1370 ]
1371 ]
1372 },
1373 "tags": [
1374 [
1375 "effective-directive",
1376 "default-src"
1377 ],
1378 [
1379 "blocked-uri",
1380 "http://evilhackerscripts.com"
1381 ],
1382 [
1383 "blocked-host",
1384 "evilhackerscripts.com"
1385 ]
1386 ],
1387 "csp": {
1388 "effective_directive": "default-src",
1389 "blocked_uri": "http://evilhackerscripts.com",
1390 "document_uri": "https://example.com/foo/bar",
1391 "original_policy": "default-src self; report-uri /csp-hotline.php",
1392 "referrer": "https://www.google.com/",
1393 "violated_directive": "default-src self"
1394 }
1395 }
1396 "###);
1397 }
1398
1399 #[test]
1400 fn test_csp_real() {
1401 let json = r#"{
1402 "csp-report": {
1403 "document-uri": "https://sentry.io/sentry/csp/issues/88513416/",
1404 "referrer": "https://sentry.io/sentry/sentry/releases/7329107476ff14cfa19cf013acd8ce47781bb93a/",
1405 "violated-directive": "script-src",
1406 "effective-directive": "script-src",
1407 "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",
1408 "disposition": "enforce",
1409 "blocked-uri": "http://baddomain.com/test.js?_=1515535030116",
1410 "line-number": 24,
1411 "column-number": 66270,
1412 "source-file": "https://e90d271df3e973c7.global.ssl.fastly.net/_static/f0c7c026a4b2a3d2b287ae2d012c9924/sentry/dist/vendor.js",
1413 "status-code": 0,
1414 "script-sample": ""
1415 }
1416 }"#;
1417
1418 let mut event = Event::default();
1419 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1420
1421 assert_annotated_snapshot!(Annotated::new(event), @r###"
1422 {
1423 "culprit": "script-src",
1424 "logentry": {
1425 "formatted": "Blocked 'script' from 'baddomain.com'"
1426 },
1427 "request": {
1428 "url": "https://sentry.io/sentry/csp/issues/88513416/",
1429 "headers": [
1430 [
1431 "Referer",
1432 "https://sentry.io/sentry/sentry/releases/7329107476ff14cfa19cf013acd8ce47781bb93a/"
1433 ]
1434 ]
1435 },
1436 "tags": [
1437 [
1438 "effective-directive",
1439 "script-src"
1440 ],
1441 [
1442 "blocked-uri",
1443 "http://baddomain.com/test.js?_=1515535030116"
1444 ],
1445 [
1446 "blocked-host",
1447 "baddomain.com"
1448 ]
1449 ],
1450 "csp": {
1451 "effective_directive": "script-src",
1452 "blocked_uri": "http://baddomain.com/test.js?_=1515535030116",
1453 "document_uri": "https://sentry.io/sentry/csp/issues/88513416/",
1454 "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",
1455 "referrer": "https://sentry.io/sentry/sentry/releases/7329107476ff14cfa19cf013acd8ce47781bb93a/",
1456 "status_code": 0,
1457 "violated_directive": "script-src",
1458 "source_file": "https://e90d271df3e973c7.global.ssl.fastly.net/_static/f0c7c026a4b2a3d2b287ae2d012c9924/sentry/dist/vendor.js",
1459 "line_number": 24,
1460 "column_number": 66270,
1461 "script_sample": "",
1462 "disposition": "enforce"
1463 }
1464 }
1465 "###);
1466 }
1467
1468 #[test]
1469 fn test_csp_culprit_0() {
1470 let json = r#"{
1471 "csp-report": {
1472 "document-uri": "http://example.com/foo",
1473 "violated-directive": "style-src http://cdn.example.com",
1474 "effective-directive": "style-src"
1475 }
1476 }"#;
1477
1478 let mut event = Event::default();
1479 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1480 insta::assert_debug_snapshot!(event.culprit, @r#""style-src http://cdn.example.com""#);
1481 }
1482
1483 #[test]
1484 fn test_csp_culprit_1() {
1485 let json = r#"{
1486 "csp-report": {
1487 "document-uri": "http://example.com/foo",
1488 "violated-directive": "style-src cdn.example.com",
1489 "effective-directive": "style-src"
1490 }
1491 }"#;
1492
1493 let mut event = Event::default();
1494 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1495 insta::assert_debug_snapshot!(event.culprit, @r#""style-src cdn.example.com""#);
1496 }
1497
1498 #[test]
1499 fn test_csp_culprit_2() {
1500 let json = r#"{
1501 "csp-report": {
1502 "document-uri": "https://example.com/foo",
1503 "violated-directive": "style-src cdn.example.com",
1504 "effective-directive": "style-src"
1505 }
1506 }"#;
1507
1508 let mut event = Event::default();
1509 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1510 insta::assert_debug_snapshot!(event.culprit, @r#""style-src cdn.example.com""#);
1511 }
1512
1513 #[test]
1514 fn test_csp_culprit_3() {
1515 let json = r#"{
1516 "csp-report": {
1517 "document-uri": "http://example.com/foo",
1518 "violated-directive": "style-src https://cdn.example.com",
1519 "effective-directive": "style-src"
1520 }
1521 }"#;
1522
1523 let mut event = Event::default();
1524 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1525 insta::assert_debug_snapshot!(event.culprit, @r#""style-src https://cdn.example.com""#);
1526 }
1527
1528 #[test]
1529 fn test_csp_culprit_4() {
1530 let json = r#"{
1531 "csp-report": {
1532 "document-uri": "http://example.com/foo",
1533 "violated-directive": "style-src http://example.com",
1534 "effective-directive": "style-src"
1535 }
1536 }"#;
1537
1538 let mut event = Event::default();
1539 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1540 insta::assert_debug_snapshot!(event.culprit, @r#""style-src 'self'""#);
1541 }
1542
1543 #[test]
1544 fn test_csp_culprit_5() {
1545 let json = r#"{
1546 "csp-report": {
1547 "document-uri": "http://example.com/foo",
1548 "violated-directive": "style-src http://example2.com example.com",
1549 "effective-directive": "style-src"
1550 }
1551 }"#;
1552
1553 let mut event = Event::default();
1554 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1555 insta::assert_debug_snapshot!(event.culprit, @r#""style-src http://example2.com 'self'""#);
1556 }
1557
1558 #[test]
1559 fn test_csp_culprit_uri_without_scheme() {
1560 let json = r#"{
1562 "csp-report": {
1563 "document-uri": "example.com",
1564 "violated-directive": "style-src example2.com"
1565 }
1566 }"#;
1567
1568 let mut event = Event::default();
1569 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1570 insta::assert_debug_snapshot!(event.culprit, @r#""style-src example2.com""#);
1571 }
1572
1573 #[test]
1574 fn test_csp_tags_stripe() {
1575 let json = r#"{
1579 "csp-report": {
1580 "document-uri": "https://example.com",
1581 "blocked-uri": "https://api.stripe.com/v1/tokens?card[number]=xxx",
1582 "effective-directive": "script-src"
1583 }
1584 }"#;
1585
1586 let mut event = Event::default();
1587 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1588 insta::assert_debug_snapshot!(event.tags, @r###"
1589 Tags(
1590 PairList(
1591 [
1592 TagEntry(
1593 "effective-directive",
1594 "script-src",
1595 ),
1596 TagEntry(
1597 "blocked-uri",
1598 "https://api.stripe.com/v1/tokens",
1599 ),
1600 TagEntry(
1601 "blocked-host",
1602 "api.stripe.com",
1603 ),
1604 ],
1605 ),
1606 )
1607 "###);
1608 }
1609
1610 #[test]
1611 fn test_csp_get_message_0() {
1612 let json = r#"{
1613 "csp-report": {
1614 "document-uri": "http://example.com/foo",
1615 "effective-directive": "img-src",
1616 "blocked-uri": "http://google.com/foo"
1617 }
1618 }"#;
1619
1620 let mut event = Event::default();
1621 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1622 let message = &event.logentry.value().unwrap().formatted;
1623 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'image' from 'google.com'""#);
1624 }
1625
1626 #[test]
1627 fn test_csp_get_message_1() {
1628 let json = r#"{
1629 "csp-report": {
1630 "document-uri": "http://example.com/foo",
1631 "effective-directive": "style-src",
1632 "blocked-uri": ""
1633 }
1634 }"#;
1635
1636 let mut event = Event::default();
1637 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1638 let message = &event.logentry.value().unwrap().formatted;
1639 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked inline 'style'""#);
1640 }
1641
1642 #[test]
1643 fn test_csp_get_message_2() {
1644 let json = r#"{
1645 "csp-report": {
1646 "document-uri": "http://example.com/foo",
1647 "effective-directive": "script-src",
1648 "blocked-uri": "",
1649 "violated-directive": "script-src 'unsafe-inline'"
1650 }
1651 }"#;
1652
1653 let mut event = Event::default();
1654 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1655 let message = &event.logentry.value().unwrap().formatted;
1656 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked unsafe inline 'script'""#);
1657 }
1658
1659 #[test]
1660 fn test_csp_get_message_3() {
1661 let json = r#"{
1662 "csp-report": {
1663 "document-uri": "http://example.com/foo",
1664 "effective-directive": "script-src",
1665 "blocked-uri": "",
1666 "violated-directive": "script-src 'unsafe-eval'"
1667 }
1668 }"#;
1669
1670 let mut event = Event::default();
1671 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1672 let message = &event.logentry.value().unwrap().formatted;
1673 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked unsafe eval() 'script'""#);
1674 }
1675
1676 #[test]
1677 fn test_csp_get_message_4() {
1678 let json = r#"{
1679 "csp-report": {
1680 "document-uri": "http://example.com/foo",
1681 "effective-directive": "script-src",
1682 "blocked-uri": "",
1683 "violated-directive": "script-src example.com"
1684 }
1685 }"#;
1686
1687 let mut event = Event::default();
1688 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1689 let message = &event.logentry.value().unwrap().formatted;
1690 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked unsafe (eval() or inline) 'script'""#);
1691 }
1692
1693 #[test]
1694 fn test_csp_get_message_5() {
1695 let json = r#"{
1696 "csp-report": {
1697 "document-uri": "http://example.com/foo",
1698 "effective-directive": "script-src",
1699 "blocked-uri": "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D"
1700 }
1701 }"#;
1702
1703 let mut event = Event::default();
1704 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1705 let message = &event.logentry.value().unwrap().formatted;
1706 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'script' from 'data:'""#);
1707 }
1708
1709 #[test]
1710 fn test_csp_get_message_6() {
1711 let json = r#"{
1712 "csp-report": {
1713 "document-uri": "http://example.com/foo",
1714 "effective-directive": "script-src",
1715 "blocked-uri": "data"
1716 }
1717 }"#;
1718
1719 let mut event = Event::default();
1720 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1721 let message = &event.logentry.value().unwrap().formatted;
1722 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'script' from 'data:'""#);
1723 }
1724
1725 #[test]
1726 fn test_csp_get_message_7() {
1727 let json = r#"{
1728 "csp-report": {
1729 "document-uri": "http://example.com/foo",
1730 "effective-directive": "style-src-elem",
1731 "blocked-uri": "http://fonts.google.com/foo"
1732 }
1733 }"#;
1734
1735 let mut event = Event::default();
1736 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1737 let message = &event.logentry.value().unwrap().formatted;
1738 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'style' from 'fonts.google.com'""#);
1739 }
1740
1741 #[test]
1742 fn test_csp_get_message_8() {
1743 let json = r#"{
1744 "csp-report": {
1745 "document-uri": "http://example.com/foo",
1746 "effective-directive": "script-src-elem",
1747 "blocked-uri": "http://cdn.ajaxapis.com/foo"
1748 }
1749 }"#;
1750
1751 let mut event = Event::default();
1752 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1753 let message = &event.logentry.value().unwrap().formatted;
1754 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'script' from 'cdn.ajaxapis.com'""#);
1755 }
1756
1757 #[test]
1758 fn test_csp_get_message_9() {
1759 let json = r#"{
1760 "csp-report": {
1761 "document-uri": "http://notlocalhost:8000/",
1762 "effective-directive": "style-src",
1763 "blocked-uri": "http://notlocalhost:8000/lol.css"
1764 }
1765 }"#;
1766
1767 let mut event = Event::default();
1768 Csp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1769 let message = &event.logentry.value().unwrap().formatted;
1770 insta::assert_debug_snapshot!(message.as_str().unwrap(), @r#""Blocked 'style' from 'notlocalhost:8000'""#);
1771 }
1772
1773 #[test]
1774 fn test_expectct_basic() {
1775 let json = r#"{
1776 "expect-ct-report": {
1777 "date-time": "2014-04-06T13:00:50Z",
1778 "hostname": "www.example.com",
1779 "port": 443,
1780 "scheme": "https",
1781 "effective-expiration-date": "2014-05-01T12:40:50Z",
1782 "served-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1783 "validated-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1784 "scts": [
1785 {
1786 "version": 1,
1787 "status": "invalid",
1788 "source": "embedded",
1789 "serialized_sct": "ABCD=="
1790 }
1791 ],
1792 "failure-mode": "enforce",
1793 "test-report": false
1794 }
1795 }"#;
1796
1797 let mut event = Event::default();
1798 ExpectCt::apply_to_event(json.as_bytes(), &mut event).unwrap();
1799 assert_annotated_snapshot!(Annotated::new(event), @r#"
1800 {
1801 "culprit": "www.example.com",
1802 "logentry": {
1803 "formatted": "Expect-CT failed for 'www.example.com'"
1804 },
1805 "request": {
1806 "url": "www.example.com"
1807 },
1808 "tags": [
1809 [
1810 "hostname",
1811 "www.example.com"
1812 ],
1813 [
1814 "port",
1815 "443"
1816 ]
1817 ],
1818 "expectct": {
1819 "date_time": "2014-04-06T13:00:50+00:00",
1820 "hostname": "www.example.com",
1821 "port": 443,
1822 "scheme": "https",
1823 "effective_expiration_date": "2014-05-01T12:40:50+00:00",
1824 "served_certificate_chain": [
1825 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1826 ],
1827 "validated_certificate_chain": [
1828 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1829 ],
1830 "scts": [
1831 {
1832 "version": 1,
1833 "status": "invalid",
1834 "source": "embedded",
1835 "serialized_sct": "ABCD=="
1836 }
1837 ],
1838 "failure_mode": "enforce",
1839 "test_report": false
1840 }
1841 }
1842 "#);
1843 }
1844
1845 #[test]
1846 fn test_expectct_invalid() {
1847 let json = r#"{
1848 "hostname": "www.example.com",
1849 "date_time": "Not an RFC3339 datetime"
1850 }"#;
1851
1852 let mut event = Event::default();
1853 ExpectCt::apply_to_event(json.as_bytes(), &mut event)
1854 .expect_err("date_time should fail to parse");
1855 }
1856
1857 #[test]
1858 fn test_expectstaple_basic() {
1859 let json = r#"{
1860 "expect-staple-report": {
1861 "date-time": "2014-04-06T13:00:50Z",
1862 "hostname": "www.example.com",
1863 "port": 443,
1864 "response-status": "ERROR_RESPONSE",
1865 "cert-status": "REVOKED",
1866 "effective-expiration-date": "2014-05-01T12:40:50Z",
1867 "served-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1868 "validated-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"]
1869 }
1870 }"#;
1871
1872 let mut event = Event::default();
1873 ExpectStaple::apply_to_event(json.as_bytes(), &mut event).unwrap();
1874 assert_annotated_snapshot!(Annotated::new(event), @r#"
1875 {
1876 "culprit": "www.example.com",
1877 "logentry": {
1878 "formatted": "Expect-Staple failed for 'www.example.com'"
1879 },
1880 "request": {
1881 "url": "www.example.com"
1882 },
1883 "tags": [
1884 [
1885 "hostname",
1886 "www.example.com"
1887 ],
1888 [
1889 "port",
1890 "443"
1891 ],
1892 [
1893 "response_status",
1894 "ERROR_RESPONSE"
1895 ],
1896 [
1897 "cert_status",
1898 "REVOKED"
1899 ]
1900 ],
1901 "expectstaple": {
1902 "date_time": "2014-04-06T13:00:50+00:00",
1903 "hostname": "www.example.com",
1904 "port": 443,
1905 "effective_expiration_date": "2014-05-01T12:40:50+00:00",
1906 "response_status": "ERROR_RESPONSE",
1907 "cert_status": "REVOKED",
1908 "served_certificate_chain": [
1909 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1910 ],
1911 "validated_certificate_chain": [
1912 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1913 ]
1914 }
1915 }
1916 "#);
1917 }
1918
1919 #[test]
1920 fn test_hpkp_basic() {
1921 let json = r#"{
1922 "date-time": "2014-04-06T13:00:50Z",
1923 "hostname": "example.com",
1924 "port": 443,
1925 "effective-expiration-date": "2014-05-01T12:40:50Z",
1926 "include-subdomains": false,
1927 "served-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1928 "validated-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
1929 "known-pins": ["pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\""]
1930 }"#;
1931
1932 let mut event = Event::default();
1933 Hpkp::apply_to_event(json.as_bytes(), &mut event).unwrap();
1934 assert_annotated_snapshot!(Annotated::new(event), @r#"
1935 {
1936 "logentry": {
1937 "formatted": "Public key pinning validation failed for 'example.com'"
1938 },
1939 "request": {
1940 "url": "example.com"
1941 },
1942 "tags": [
1943 [
1944 "hostname",
1945 "example.com"
1946 ],
1947 [
1948 "port",
1949 "443"
1950 ],
1951 [
1952 "include-subdomains",
1953 "false"
1954 ]
1955 ],
1956 "hpkp": {
1957 "date_time": "2014-04-06T13:00:50+00:00",
1958 "hostname": "example.com",
1959 "port": 443,
1960 "effective_expiration_date": "2014-05-01T12:40:50+00:00",
1961 "include_subdomains": false,
1962 "served_certificate_chain": [
1963 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1964 ],
1965 "validated_certificate_chain": [
1966 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
1967 ],
1968 "known_pins": [
1969 "pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\""
1970 ]
1971 }
1972 }
1973 "#);
1974 }
1975
1976 #[test]
1977 fn test_security_report_type_deserializer_recognizes_csp_reports() {
1978 let csp_report_text = r#"{
1979 "csp-report": {
1980 "document-uri": "https://example.com/foo/bar",
1981 "referrer": "https://www.google.com/",
1982 "violated-directive": "default-src self",
1983 "original-policy": "default-src self; report-uri /csp-hotline.php",
1984 "blocked-uri": "http://evilhackerscripts.com"
1985 }
1986 }"#;
1987
1988 let report_type = SecurityReportType::from_json(csp_report_text.as_bytes()).unwrap();
1989 assert_eq!(report_type, Some(SecurityReportType::Csp));
1990 }
1991
1992 #[test]
1993 fn test_security_report_type_deserializer_recognizes_csp_violations_reports() {
1994 let csp_report_text = r#"{
1995 "age":0,
1996 "body":{
1997 "blockedURL":"https://example.com/tst/media/7_del.png",
1998 "disposition":"enforce",
1999 "documentURL":"https://example.com/tst/test_frame.php?ID=229&hash=da964209653e467d337313e51876e27d",
2000 "effectiveDirective":"img-src",
2001 "lineNumber":9,
2002 "originalPolicy":"default-src 'none'; report-to endpoint-csp;",
2003 "referrer":"https://example.com/test229/",
2004 "sourceFile":"https://example.com/tst/test_frame.php?ID=229&hash=da964209653e467d337313e51876e27d",
2005 "statusCode":0
2006 },
2007 "type":"csp-violation",
2008 "url":"https://example.com/tst/test_frame.php?ID=229&hash=da964209653e467d337313e51876e27d",
2009 "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"
2010 }"#;
2011
2012 let report_type = SecurityReportType::from_json(csp_report_text.as_bytes()).unwrap();
2013 assert_eq!(report_type, Some(SecurityReportType::Csp));
2014 }
2015
2016 #[test]
2017 fn test_security_report_type_deserializer_recognizes_expect_ct_reports() {
2018 let expect_ct_report_text = r#"{
2019 "expect-ct-report": {
2020 "date-time": "2014-04-06T13:00:50Z",
2021 "hostname": "www.example.com",
2022 "port": 443,
2023 "effective-expiration-date": "2014-05-01T12:40:50Z",
2024 "served-certificate-chain": [
2025 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
2026 ],
2027 "validated-certificate-chain": [
2028 "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"
2029 ],
2030 "scts": [
2031 {
2032 "version": 1,
2033 "status": "invalid",
2034 "source": "embedded",
2035 "serialized_sct": "ABCD=="
2036 }
2037 ]
2038 }
2039 }"#;
2040
2041 let report_type = SecurityReportType::from_json(expect_ct_report_text.as_bytes()).unwrap();
2042 assert_eq!(report_type, Some(SecurityReportType::ExpectCt));
2043 }
2044
2045 #[test]
2046 fn test_security_report_type_deserializer_recognizes_expect_staple_reports() {
2047 let expect_staple_report_text = r#"{
2048 "expect-staple-report": {
2049 "date-time": "2014-04-06T13:00:50Z",
2050 "hostname": "www.example.com",
2051 "port": 443,
2052 "response-status": "ERROR_RESPONSE",
2053 "cert-status": "REVOKED",
2054 "effective-expiration-date": "2014-05-01T12:40:50Z",
2055 "served-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"],
2056 "validated-certificate-chain": ["-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"]
2057 }
2058 }"#;
2059 let report_type =
2060 SecurityReportType::from_json(expect_staple_report_text.as_bytes()).unwrap();
2061 assert_eq!(report_type, Some(SecurityReportType::ExpectStaple));
2062 }
2063
2064 #[test]
2065 fn test_security_report_type_deserializer_recognizes_hpkp_reports() {
2066 let hpkp_report_text = r#"{
2067 "date-time": "2014-04-06T13:00:50Z",
2068 "hostname": "www.example.com",
2069 "port": 443,
2070 "effective-expiration-date": "2014-05-01T12:40:50Z",
2071 "include-subdomains": false,
2072 "served-certificate-chain": [
2073 "-----BEGIN CERTIFICATE-----\n MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\n... -----END CERTIFICATE-----"
2074 ],
2075 "validated-certificate-chain": [
2076 "-----BEGIN CERTIFICATE-----\n MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\n... -----END CERTIFICATE-----"
2077 ],
2078 "known-pins": [
2079 "pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\"",
2080 "pin-sha256=\"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=\""
2081 ]
2082 }"#;
2083
2084 let report_type = SecurityReportType::from_json(hpkp_report_text.as_bytes()).unwrap();
2085 assert_eq!(report_type, Some(SecurityReportType::Hpkp));
2086 }
2087
2088 #[test]
2089 fn test_effective_directive_from_violated_directive_single() {
2090 let csp_raw: CspRaw =
2092 serde_json::from_str(r#"{"violated-directive":"default-src"}"#).unwrap();
2093 assert!(matches!(
2094 csp_raw.effective_directive(),
2095 Ok(CspDirective::DefaultSrc)
2096 ));
2097 }
2098
2099 #[test]
2100 fn test_extract_effective_directive_from_long_form() {
2101 let json = r#"{
2103 "csp-report": {
2104 "document-uri": "http://example.com/foo",
2105 "effective-directive": "script-src 'report-sample' 'strict-dynamic' 'unsafe-eval' 'nonce-random" ,
2106 "blocked-uri": "data"
2107 }
2108 }"#;
2109
2110 let raw_report = serde_json::from_slice::<CspReportRaw>(json.as_bytes()).unwrap();
2111 let raw_csp = raw_report.csp_report;
2112
2113 let effective_directive = raw_csp.effective_directive().unwrap();
2114
2115 assert_eq!(effective_directive, CspDirective::ScriptSrc);
2116
2117 let json = r#"{
2119 "csp-report": {
2120 "document-uri": "http://example.com/foo",
2121 "violated-directive": "script-src 'report-sample' 'strict-dynamic' 'unsafe-eval' 'nonce-random" ,
2122 "blocked-uri": "data"
2123 }
2124 }"#;
2125
2126 let raw_report = serde_json::from_slice::<CspReportRaw>(json.as_bytes()).unwrap();
2127 let raw_csp = raw_report.csp_report;
2128
2129 let effective_directive = raw_csp.effective_directive().unwrap();
2130
2131 assert_eq!(effective_directive, CspDirective::ScriptSrc);
2132 }
2133
2134 #[test]
2135 fn test_csp_report_fenced_frame_src() {
2136 let json = r#"
2137 {
2138 "csp-report": {
2139 "document-uri": "https://example.com",
2140 "referrer": "https://www.google.com/",
2141 "violated-directive": "fenced-frame-src",
2142 "effective-directive": "fenced-frame-src",
2143 "original-policy": "default-src 'self' 'unsafe-eval'",
2144 "disposition": "enforce",
2145 "blocked-uri": "",
2146 "status-code": 200,
2147 "script-sample": ""
2148 }
2149 }
2150 "#;
2151 let raw_report = serde_json::from_slice::<CspReportRaw>(json.as_bytes()).unwrap();
2152 let raw_csp = raw_report.csp_report;
2153
2154 assert!(raw_csp.effective_directive().is_ok());
2155 }
2156}