relay_server/services/processor/
attachment.rs

1//! Attachments processor code.
2
3use 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/// Adds processing placeholders for special attachments.
24///
25/// If special attachments are present in the envelope, this adds placeholder payloads to the
26/// event. This indicates to the pipeline that the event needs special processing.
27///
28/// If the event payload was empty before, it is created.
29#[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
56/// Apply data privacy rules to attachments in the envelope.
57///
58/// This only applies the new PII rules that explicitly select `ValueType::Binary` or one of the
59/// attachment types. When special attachments are detected, these are scrubbed with custom
60/// logic; otherwise the entire attachment is treated as a single binary blob.
61pub 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                // We temporarily only scrub attachments to projects that have at least one simple attachment rule,
77                // such as `$attachments.'foo.txt'`.
78                // After we have assessed the impact on performance we can relax this condition.
79                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    // Minidump scrubbing can fail if the minidump cannot be parsed. In this case, we
93    // must be conservative and treat it as a plain attachment. Under extreme
94    // conditions, this could destroy stack memory.
95    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}