relay_server/services/processor/
attachment.rs1use std::error::Error;
4use std::time::Instant;
5
6use relay_pii::{PiiAttachmentsProcessor, SelectorPathItem, SelectorSpec};
7use relay_statsd::metric;
8
9use crate::envelope::{AttachmentType, ContentType, ItemType};
10use crate::statsd::RelayTimers;
11
12use crate::managed::TypedEnvelope;
13use crate::services::projects::project::ProjectInfo;
14use relay_dynamic_config::Feature;
15#[cfg(feature = "processing")]
16use {
17 crate::services::processor::{ErrorGroup, EventFullyNormalized},
18 crate::utils,
19 relay_event_schema::protocol::{Event, Metrics},
20 relay_protocol::Annotated,
21};
22
23#[cfg(feature = "processing")]
30pub fn create_placeholders(
31 managed_envelope: &mut TypedEnvelope<ErrorGroup>,
32 event: &mut Annotated<Event>,
33 metrics: &mut Metrics,
34) -> Option<EventFullyNormalized> {
35 let envelope = managed_envelope.envelope();
36 let minidump_attachment =
37 envelope.get_item_by(|item| item.attachment_type() == Some(&AttachmentType::Minidump));
38 let apple_crash_report_attachment = envelope
39 .get_item_by(|item| item.attachment_type() == Some(&AttachmentType::AppleCrashReport));
40
41 if let Some(item) = minidump_attachment {
42 let event = event.get_or_insert_with(Event::default);
43 metrics.bytes_ingested_event_minidump = Annotated::new(item.len() as u64);
44 utils::process_minidump(event, &item.payload());
45 return Some(EventFullyNormalized(false));
46 } else if let Some(item) = apple_crash_report_attachment {
47 let event = event.get_or_insert_with(Event::default);
48 metrics.bytes_ingested_event_applecrashreport = Annotated::new(item.len() as u64);
49 utils::process_apple_crash_report(event, &item.payload());
50 return Some(EventFullyNormalized(false));
51 }
52
53 None
54}
55
56pub fn scrub<Group>(managed_envelope: &mut TypedEnvelope<Group>, project_info: &ProjectInfo) {
62 let envelope = managed_envelope.envelope_mut();
63 if let Some(ref config) = project_info.config.pii_config {
64 let view_hierarchy_scrubbing_enabled = project_info
65 .config
66 .features
67 .has(Feature::ViewHierarchyScrubbing);
68 for item in envelope.items_mut() {
69 if view_hierarchy_scrubbing_enabled
70 && item.attachment_type() == Some(&AttachmentType::ViewHierarchy)
71 {
72 scrub_view_hierarchy(item, config)
73 } else if item.attachment_type() == Some(&AttachmentType::Minidump) {
74 scrub_minidump(item, config)
75 } else if item.ty() == &ItemType::Attachment && has_simple_attachment_selector(config) {
76 scrub_attachment(item, config)
80 }
81 }
82 }
83}
84
85fn scrub_minidump(item: &mut crate::envelope::Item, config: &relay_pii::PiiConfig) {
86 debug_assert_eq!(item.attachment_type(), Some(&AttachmentType::Minidump));
87 let filename = item.filename().unwrap_or_default();
88 let mut payload = item.payload().to_vec();
89
90 let processor = PiiAttachmentsProcessor::new(config.compiled());
91
92 let start = Instant::now();
96 match processor.scrub_minidump(filename, &mut payload) {
97 Ok(modified) => {
98 metric!(
99 timer(RelayTimers::MinidumpScrubbing) = start.elapsed(),
100 status = if modified { "ok" } else { "n/a" },
101 );
102 }
103 Err(scrub_error) => {
104 metric!(
105 timer(RelayTimers::MinidumpScrubbing) = start.elapsed(),
106 status = "error"
107 );
108 relay_log::debug!(
109 error = &scrub_error as &dyn Error,
110 "failed to scrub minidump",
111 );
112 metric!(
113 timer(RelayTimers::AttachmentScrubbing),
114 attachment_type = "minidump",
115 {
116 processor.scrub_attachment(filename, &mut payload);
117 }
118 )
119 }
120 }
121
122 let content_type = item
123 .content_type()
124 .unwrap_or(&ContentType::Minidump)
125 .clone();
126
127 item.set_payload(content_type, payload);
128}
129
130fn scrub_view_hierarchy(item: &mut crate::envelope::Item, config: &relay_pii::PiiConfig) {
131 let processor = PiiAttachmentsProcessor::new(config.compiled());
132
133 let payload = item.payload();
134 let start = Instant::now();
135 match processor.scrub_json(&payload) {
136 Ok(output) => {
137 metric!(
138 timer(RelayTimers::ViewHierarchyScrubbing) = start.elapsed(),
139 status = "ok"
140 );
141 let content_type = item.content_type().unwrap_or(&ContentType::Json).clone();
142 item.set_payload(content_type, output);
143 }
144 Err(e) => {
145 relay_log::debug!(error = &e as &dyn Error, "failed to scrub view hierarchy",);
146 metric!(
147 timer(RelayTimers::ViewHierarchyScrubbing) = start.elapsed(),
148 status = "error"
149 )
150 }
151 }
152}
153
154fn has_simple_attachment_selector(config: &relay_pii::PiiConfig) -> bool {
155 for application in &config.applications {
156 if let SelectorSpec::Path(vec) = &application.0 {
157 let Some([a, b]) = vec.get(0..2) else {
158 continue;
159 };
160 if matches!(
161 a,
162 SelectorPathItem::Type(relay_event_schema::processor::ValueType::Attachments)
163 ) && matches!(b, SelectorPathItem::Key(_))
164 {
165 return true;
166 }
167 }
168 }
169 false
170}
171
172fn scrub_attachment(item: &mut crate::envelope::Item, config: &relay_pii::PiiConfig) {
173 let filename = item.filename().unwrap_or_default();
174 let mut payload = item.payload().to_vec();
175
176 let processor = PiiAttachmentsProcessor::new(config.compiled());
177 let attachment_type_tag = match item.attachment_type() {
178 Some(t) => t.to_string(),
179 None => "".to_owned(),
180 };
181 metric!(
182 timer(RelayTimers::AttachmentScrubbing),
183 attachment_type = &attachment_type_tag,
184 {
185 processor.scrub_attachment(filename, &mut payload);
186 }
187 );
188
189 item.set_payload_without_content_type(payload);
190}
191
192#[cfg(test)]
193mod tests {
194 use relay_pii::PiiConfig;
195
196 use super::*;
197
198 #[test]
199 fn matches_attachment_selector() {
200 let config = r#"{
201 "rules": {"0": {"type": "ip", "redaction": {"method": "remove"}}},
202 "applications": {"$attachments.'foo.txt'": ["0"]}
203 }"#;
204 let config: PiiConfig = serde_json::from_str(config).unwrap();
205 assert!(has_simple_attachment_selector(&config));
206 }
207
208 #[test]
209 fn does_not_match_wildcard() {
210 let config = r#"{
211 "rules": {},
212 "applications": {"$attachments.**":["0"]}
213 }"#;
214 let config: PiiConfig = serde_json::from_str(config).unwrap();
215 assert!(!has_simple_attachment_selector(&config));
216 }
217
218 #[test]
219 fn does_not_match_empty() {
220 let config = r#"{
221 "rules": {},
222 "applications": {}
223 }"#;
224 let config: PiiConfig = serde_json::from_str(config).unwrap();
225 assert!(!has_simple_attachment_selector(&config));
226 }
227
228 #[test]
229 fn does_not_match_something_else() {
230 let config = r#"{
231 "rules": {},
232 "applications": {
233 "**": ["0"]
234 }
235 }"#;
236 let config: PiiConfig = serde_json::from_str(config).unwrap();
237 assert!(!has_simple_attachment_selector(&config));
238 }
239}