saluki_components/transforms/trace_obfuscation/
mod.rs

1//! Trace obfuscation transform.
2
3mod credit_cards;
4mod http;
5mod json;
6mod memcached;
7mod obfuscator;
8mod redis;
9mod sql;
10mod sql_filters;
11mod sql_tokenizer;
12
13use agent_data_plane_config::domains;
14use async_trait::async_trait;
15use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
16use saluki_core::{
17    components::{transforms::*, ComponentContext},
18    data_model::event::{
19        trace::{AttributeValue, Span},
20        Event,
21    },
22    topology::EventsBuffer,
23};
24use saluki_error::GenericError;
25use stringtheory::MetaString;
26
27pub use self::obfuscator::{tags, ObfuscationConfig, Obfuscator};
28
29const TEXT_NON_PARSABLE_SQL: &str = "Non-parsable SQL query";
30
31/// Trace obfuscation configuration.
32pub struct TraceObfuscationConfiguration {
33    /// Obfuscator configuration.
34    pub config: ObfuscationConfig,
35}
36
37impl TraceObfuscationConfiguration {
38    /// Creates a new `TraceObfuscationConfiguration` from the resolved trace configuration.
39    pub fn from_configuration(config: &domains::traces::Obfuscation) -> Self {
40        Self { config: config.into() }
41    }
42}
43
44#[async_trait]
45impl SynchronousTransformBuilder for TraceObfuscationConfiguration {
46    async fn build(&self, _context: ComponentContext) -> Result<Box<dyn SynchronousTransform + Send>, GenericError> {
47        Ok(Box::new(TraceObfuscation {
48            obfuscator: Obfuscator::new(self.config.clone()),
49        }))
50    }
51}
52
53impl MemoryBounds for TraceObfuscationConfiguration {
54    fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
55        builder
56            .minimum()
57            .with_single_value::<TraceObfuscation>("component struct");
58    }
59}
60
61/// The obfuscation transform that processes traces.
62pub struct TraceObfuscation {
63    obfuscator: Obfuscator,
64}
65
66impl TraceObfuscation {
67    fn obfuscate_span(&mut self, span: &mut Span) {
68        if self.obfuscator.config.credit_cards.enabled {
69            self.obfuscate_credit_cards_in_span(span);
70        }
71
72        match span.span_type() {
73            "http" | "web" => self.obfuscate_http_span(span),
74            "sql" | "cassandra" => self.obfuscate_sql_span(span),
75            "redis" | "valkey" => self.obfuscate_redis_span(span),
76            "memcached" => self.obfuscate_memcached_span(span),
77            "mongodb" => self.obfuscate_mongodb_span(span),
78            "elasticsearch" | "opensearch" => self.obfuscate_elasticsearch_span(span),
79            _ => {}
80        }
81    }
82
83    fn obfuscate_credit_cards_in_span(&mut self, span: &mut Span) {
84        for (key, value) in span.attributes.iter_mut() {
85            if let AttributeValue::String(str_val) = value {
86                if let Some(replacement) = self
87                    .obfuscator
88                    .obfuscate_credit_card_number(key.as_ref(), str_val.as_ref())
89                {
90                    *str_val = replacement;
91                }
92            }
93        }
94    }
95
96    fn obfuscate_http_span(&mut self, span: &mut Span) {
97        let url_value = match span.attributes.get(tags::HTTP_URL).and_then(AttributeValue::as_string) {
98            Some(v) if !v.is_empty() => v.as_ref().to_owned(),
99            _ => return,
100        };
101
102        if let Some(obfuscated) = self.obfuscator.obfuscate_url(&url_value) {
103            span.attributes
104                .insert(tags::HTTP_URL.into(), AttributeValue::String(obfuscated));
105        }
106    }
107
108    fn obfuscate_sql_span(&mut self, span: &mut Span) {
109        let sql_query_owned: Option<String> = span
110            .attributes
111            .get(tags::DB_STATEMENT)
112            .and_then(AttributeValue::as_string)
113            .filter(|s| !s.is_empty())
114            .map(|s| s.as_ref().to_owned());
115        let sql_query: &str = match &sql_query_owned {
116            Some(s) => s.as_str(),
117            None => span.resource(),
118        };
119
120        if sql_query.is_empty() {
121            return;
122        }
123
124        let dbms_owned: Option<String> = span
125            .attributes
126            .get(tags::DBMS)
127            .and_then(AttributeValue::as_string)
128            .filter(|s| !s.is_empty())
129            .map(|s| s.as_ref().to_owned());
130
131        let config = match &dbms_owned {
132            Some(d) => self.obfuscator.config.sql.with_dbms(d.clone()),
133            None => self.obfuscator.config.sql.clone(),
134        };
135
136        match sql::obfuscate_sql_string(sql_query, &config) {
137            Ok(obfuscated) => {
138                let query: MetaString = obfuscated.query.into();
139
140                span.set_resource(query.clone());
141                span.attributes
142                    .insert(tags::SQL_QUERY.into(), AttributeValue::String(query.clone()));
143
144                if span.attributes.contains_key(tags::DB_STATEMENT) {
145                    span.attributes
146                        .insert(tags::DB_STATEMENT.into(), AttributeValue::String(query));
147                }
148
149                if !obfuscated.table_names.is_empty() {
150                    span.attributes.insert(
151                        "sql.tables".into(),
152                        AttributeValue::String(obfuscated.table_names.into()),
153                    );
154                }
155            }
156            Err(_) => {
157                let non_parsable: MetaString = TEXT_NON_PARSABLE_SQL.into();
158                span.set_resource(non_parsable.clone());
159                span.attributes
160                    .insert(tags::SQL_QUERY.into(), AttributeValue::String(non_parsable));
161            }
162        }
163    }
164
165    fn obfuscate_redis_span(&mut self, span: &mut Span) {
166        let resource = span.resource();
167        if resource.is_empty() {
168            return;
169        }
170
171        if let Some(quantized) = self.obfuscator.quantize_redis_string(resource) {
172            span.set_resource(quantized.to_string());
173        }
174
175        if span.span_type() == "redis" && self.obfuscator.config.redis.enabled {
176            if let Some(cmd_value) = span
177                .attributes
178                .get(tags::REDIS_RAW_COMMAND)
179                .and_then(AttributeValue::as_string)
180                .map(|s| s.as_ref().to_owned())
181            {
182                if let Some(obfuscated) = self.obfuscator.obfuscate_redis_string(&cmd_value) {
183                    span.attributes
184                        .insert(tags::REDIS_RAW_COMMAND.into(), AttributeValue::String(obfuscated));
185                }
186            }
187        }
188
189        if span.span_type() == "valkey" && self.obfuscator.config.valkey.enabled {
190            if let Some(cmd_value) = span
191                .attributes
192                .get(tags::VALKEY_RAW_COMMAND)
193                .and_then(AttributeValue::as_string)
194                .map(|s| s.as_ref().to_owned())
195            {
196                if let Some(obfuscated) = self.obfuscator.obfuscate_valkey_string(&cmd_value) {
197                    span.attributes
198                        .insert(tags::VALKEY_RAW_COMMAND.into(), AttributeValue::String(obfuscated));
199                }
200            }
201        }
202    }
203
204    fn obfuscate_memcached_span(&mut self, span: &mut Span) {
205        if !self.obfuscator.config.memcached.enabled {
206            return;
207        }
208
209        let cmd_value = match span
210            .attributes
211            .get(tags::MEMCACHED_COMMAND)
212            .and_then(AttributeValue::as_string)
213        {
214            Some(v) if !v.is_empty() => v.as_ref().to_owned(),
215            _ => return,
216        };
217
218        if let Some(obfuscated) = self.obfuscator.obfuscate_memcached_command(&cmd_value) {
219            if obfuscated.is_empty() {
220                span.attributes.remove(tags::MEMCACHED_COMMAND);
221            } else {
222                span.attributes
223                    .insert(tags::MEMCACHED_COMMAND.into(), AttributeValue::String(obfuscated));
224            }
225        }
226    }
227
228    fn obfuscate_mongodb_span(&mut self, span: &mut Span) {
229        let query_value = match span
230            .attributes
231            .get(tags::MONGODB_QUERY)
232            .and_then(AttributeValue::as_string)
233        {
234            Some(v) => v.as_ref().to_owned(),
235            None => return,
236        };
237
238        if let Some(obfuscated) = self.obfuscator.obfuscate_mongodb_string(&query_value) {
239            span.attributes
240                .insert(tags::MONGODB_QUERY.into(), AttributeValue::String(obfuscated));
241        }
242    }
243
244    fn obfuscate_elasticsearch_span(&mut self, span: &mut Span) {
245        if let Some(body_value) = span
246            .attributes
247            .get(tags::ELASTIC_BODY)
248            .and_then(AttributeValue::as_string)
249            .map(|s| s.as_ref().to_owned())
250        {
251            if let Some(obfuscated) = self.obfuscator.obfuscate_elasticsearch_string(&body_value) {
252                span.attributes
253                    .insert(tags::ELASTIC_BODY.into(), AttributeValue::String(obfuscated));
254            }
255        }
256
257        if let Some(body_value) = span
258            .attributes
259            .get(tags::OPENSEARCH_BODY)
260            .and_then(AttributeValue::as_string)
261            .map(|s| s.as_ref().to_owned())
262        {
263            if let Some(obfuscated) = self.obfuscator.obfuscate_opensearch_string(&body_value) {
264                span.attributes
265                    .insert(tags::OPENSEARCH_BODY.into(), AttributeValue::String(obfuscated));
266            }
267        }
268    }
269}
270
271impl SynchronousTransform for TraceObfuscation {
272    fn transform_buffer(&mut self, buffer: &mut EventsBuffer) {
273        for event in buffer {
274            if let Event::Trace(ref mut trace) = event {
275                for span in trace.spans_mut() {
276                    self.obfuscate_span(span);
277                }
278            }
279        }
280    }
281}