saluki_components/transforms/trace_obfuscation/
mod.rs1mod 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
31pub struct TraceObfuscationConfiguration {
33 pub config: ObfuscationConfig,
35}
36
37impl TraceObfuscationConfiguration {
38 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
61pub 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}