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