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